<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Codango® / Codango.Com</title>
	<atom:link href="https://codango.com/feed" rel="self" type="application/rss+xml" />
	<link>https://codango.com</link>
	<description></description>
	<lastBuildDate>Thu, 17 Sep 2026 11:37:32 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://codango.com/wp-content/uploads/cropped-faviconpng-32x32.png</url>
	<title>Codango® / Codango.Com</title>
	<link>https://codango.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How I&#8217;m Prepping for Interview Assessments — Smarter and Faster</title>
		<link>https://codango.com/how-im-prepping-for-interview-assessments-smarter-and-faster.htm</link>
					<comments>https://codango.com/how-im-prepping-for-interview-assessments-smarter-and-faster.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 11:37:32 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-im-prepping-for-interview-assessments-smarter-and-faster/</guid>

					<description><![CDATA[As a developer juggling work and side projects, prepping for technical interviews can feel overwhelming. Instead of chasing perfection, I&#8217;ve built a workflow that combines brute force, learning patterns, and <a class="more-link" href="https://codango.com/how-im-prepping-for-interview-assessments-smarter-and-faster.htm">Continue reading <span class="screen-reader-text">  How I&#8217;m Prepping for Interview Assessments — Smarter and Faster</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>As a developer juggling work and side projects, prepping for technical interviews can feel overwhelming. Instead of chasing perfection, I&#8217;ve built a workflow that combines <strong>brute force, learning patterns, and AI tools</strong> — and it&#8217;s helping me <strong>learn faster and stay sharp</strong>.</p>
<p>Here’s how I’m doing it.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e0.png" alt="🧠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Step 1: Categorize Topics<br />
</p></h2>
<p>I broke down all assessment questions into <strong>6 core categories</strong>:</p>
<ul>
<li>Algorithms &amp; Data Structures</li>
<li>SQL &amp; LINQ</li>
<li>C#/.NET Core concepts</li>
<li>DevOps &amp; CI/CD</li>
<li>System Design (microservices, REST, queues)</li>
<li>AI Tools as Coding Accelerators</li>
</ul>
<p>This helps me track what to focus on weekly.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f501.png" alt="🔁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Step 2: Start with Brute Force (Then Optimize)<br />
</p></h2>
<p>Instead of panicking over efficiency, I:</p>
<ul>
<li>Write a brute-force version first</li>
<li>Ensure it works for small cases</li>
<li>Then search or iterate into the optimized version</li>
</ul>
<p>This makes patterns like <strong>sliding window, hashmap, and recursion</strong> much easier to understand.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2699.png" alt="⚙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Step 3: Practice Common Patterns<br />
</p></h2>
<p>Each day I choose a pattern like:</p>
<ul>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f501.png" alt="🔁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Sliding Window → Longest substring without repeat</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e0.png" alt="🧠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> HashMap → Two Sum, Group Anagrams</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Stack → Valid Parentheses</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c8.png" alt="📈" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Binary Search → Search in rotated array</li>
</ul>
<p>And I solve 1–2 problems using <strong>brute first, then refactor</strong>.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9ea.png" alt="🧪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Step 4: Weekly Mock Tests<br />
</p></h2>
<p>Once a week, I do a 25-minute simulation:</p>
<ul>
<li>1 algorithm problem (LeetCode or Coderbyte)</li>
<li>1 real-world backend question (design API or SQL query)</li>
<li>Use Copilot + ChatGPT <em>only for hints, not answers</em>
</li>
</ul>
<p>This gives me <strong>realistic pressure</strong> and makes me confident before the real test.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Step 5: AI Tools that Help<br />
</p></h2>
<p>Here’s how I use AI ethically:</p>
<ul>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f539.png" alt="🔹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>GitHub Copilot</strong> to autocomplete loops, LINQ</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f539.png" alt="🔹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>ChatGPT</strong> to explain logic or debug</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f539.png" alt="🔹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Ollama</strong> as a local GPT during offline prep</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f539.png" alt="🔹" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Build a Notion doc of best prompts for reuse</li>
</ul>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f5c2.png" alt="🗂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tools I Keep Open<br />
</p></h2>
<ul>
<li><a href="https://leetcode.com/discuss/general-discussion/469180/leetcode-pattern" rel="noopener noreferrer">LeetCode Patterns</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/csharp/" rel="noopener noreferrer">C# Docs</a></li>
<li><a href="https://visualgo.net/en" rel="noopener noreferrer">Visualgo.net</a></li>
<li>My own GitHub repo with reusable snippets</li>
</ul>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Final Thoughts<br />
</p></h2>
<p>By working smarter (not longer), I’ve found myself:</p>
<ul>
<li>Writing faster and cleaner code</li>
<li>Explaining my solutions with more confidence</li>
<li>Actually <strong>enjoying</strong> the interview prep process</li>
</ul>
<p>If you&#8217;re prepping too — how do you learn best?</p>
<p>Let’s share strategies and improve together. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-im-prepping-for-interview-assessments-smarter-and-faster.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Writing a real PNG compressor in vanilla JavaScript (no WASM, no libraries)</title>
		<link>https://codango.com/writing-a-real-png-compressor-in-vanilla-javascript-no-wasm-no-libraries.htm</link>
					<comments>https://codango.com/writing-a-real-png-compressor-in-vanilla-javascript-no-wasm-no-libraries.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:46:09 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/writing-a-real-png-compressor-in-vanilla-javascript-no-wasm-no-libraries.htm</guid>

					<description><![CDATA[Every online image converter I tried had the same shape: drag your file in, it uploads to a server somewhere, you get a download link back. That&#8217;s fine for a <a class="more-link" href="https://codango.com/writing-a-real-png-compressor-in-vanilla-javascript-no-wasm-no-libraries.htm">Continue reading <span class="screen-reader-text">  Writing a real PNG compressor in vanilla JavaScript (no WASM, no libraries)</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Every online image converter I tried had the same shape: drag your file in, it uploads to a server somewhere, you get a download link back.</p>
<p>That&#8217;s fine for a meme. It&#8217;s less fine for a scan of your passport, and it&#8217;s the most common thing people convert. So I wanted to know how far a browser could get on its own.</p>
<p>Quite far, as it turns out — but not without a few traps that cost me an evening each. Here are the four that were worth writing down.</p>
<h2>
<p>  The constraint<br />
</p></h2>
<p>Browsers decode plenty of formats: PNG, JPEG, WebP, GIF, BMP, AVIF, SVG, ICO.</p>
<p>They <strong>encode</strong> exactly three: PNG, JPEG, WebP. That&#8217;s the whole list <code>canvas.toBlob()</code> will give you.</p>
<p>Everything else — BMP, ICO, PDF, and crucially a <em>properly compressed</em> PNG — you write yourself, byte by byte.</p>
<h2>
<p>  Trap 1: <code>toBlob</code> lies to you<br />
</p></h2>
<p>This is the one I&#8217;d most like to have known first.</p>
<p><code>canvas.toBlob()</code> takes a MIME type. Pass it one the browser can&#8217;t encode, and it does not throw, does not return null, and does not warn. It silently gives you a <strong>PNG</strong> with a success callback.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="nx">canvas</span><span class="p">.</span><span class="nf">toBlob</span><span class="p">((</span><span class="nx">blob</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">blob</span><span class="p">.</span><span class="nx">type</span><span class="p">);</span> <span class="c1">// "image/png" — but I asked for image/avif</span>
<span class="p">},</span> <span class="dl">'</span><span class="s1">image/avif</span><span class="dl">'</span><span class="p">,</span> <span class="mf">0.8</span><span class="p">);</span>
</code></pre>
</div>
<p>So you ship a converter that appears to work perfectly, and your users get files named <code>.avif</code> that are actually PNGs. Nothing errors. You find out from a bug report.</p>
<p>The fix is two lines:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">function</span> <span class="nf">encodeNative</span><span class="p">(</span><span class="nx">canvas</span><span class="p">,</span> <span class="nx">mime</span><span class="p">,</span> <span class="nx">quality</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">canvas</span><span class="p">.</span><span class="nf">toBlob</span><span class="p">((</span><span class="nx">blob</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">blob</span> <span class="o">&amp;&amp;</span> <span class="nx">blob</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="nx">mime</span><span class="p">)</span> <span class="nf">resolve</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
      <span class="k">else</span> <span class="nf">reject</span><span class="p">(</span><span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Your browser can't export </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">mime</span><span class="p">));</span>
    <span class="p">},</span> <span class="nx">mime</span><span class="p">,</span> <span class="nx">quality</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Check the type you got against the type you asked for. Never trust the callback firing as proof of anything.</p>
<h2>
<p>  Trap 2: &#8220;compressing&#8221; a PNG usually does nothing<br />
</p></h2>
<p>Here&#8217;s the thing that surprised me most. Draw a PNG to a canvas, call <code>toBlob('image/png')</code>, and you get a file that&#8217;s often <em>larger</em> than the original.</p>
<p>PNG has no quality slider. It&#8217;s lossless. So there&#8217;s no knob for <code>toBlob</code> to turn, and re-encoding just re-does the same lossless compression, usually worse than whatever tool made the original.</p>
<p>Real PNG compression works differently. A standard PNG stores 24-bit colour — about 16 million possible values per pixel. Almost no real image uses that many. A logo might use twelve. A screenshot might use two hundred.</p>
<p>So you build an optimised palette of at most 256 colours and store a one-byte <em>index</em> per pixel instead of three or four bytes of colour. That&#8217;s where the savings live, and <code>toBlob</code> will never do it for you.</p>
<p>Which means writing the PNG yourself.</p>
<h2>
<p>  Writing a PNG by hand<br />
</p></h2>
<p>A PNG is an 8-byte signature followed by chunks. Each chunk is: length, 4-byte type, data, CRC32.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">function</span> <span class="nf">pngChunk</span><span class="p">(</span><span class="nx">type</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">out</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="mi">12</span> <span class="o">+</span> <span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DataView</span><span class="p">(</span><span class="nx">out</span><span class="p">.</span><span class="nx">buffer</span><span class="p">);</span>
  <span class="nx">v</span><span class="p">.</span><span class="nf">setUint32</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="nx">out</span><span class="p">[</span><span class="mi">4</span> <span class="o">+</span> <span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">type</span><span class="p">.</span><span class="nf">charCodeAt</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span>
  <span class="nx">out</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="mi">8</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">crcBody</span> <span class="o">=</span> <span class="nx">out</span><span class="p">.</span><span class="nf">subarray</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">8</span> <span class="o">+</span> <span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>
  <span class="nx">v</span><span class="p">.</span><span class="nf">setUint32</span><span class="p">(</span><span class="mi">8</span> <span class="o">+</span> <span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="nf">crc32</span><span class="p">(</span><span class="nx">crcBody</span><span class="p">));</span>
  <span class="k">return</span> <span class="nx">out</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Note the CRC covers the type <strong>and</strong> the data, but not the length field. Get that wrong and every decoder rejects the file with no useful message.</p>
<p>CRC32 itself is a table and a loop:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">CRC_TABLE</span> <span class="o">=</span> <span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="mi">256</span><span class="p">);</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">n</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">n</span> <span class="o">&lt;</span> <span class="mi">256</span><span class="p">;</span> <span class="nx">n</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">n</span><span class="p">;</span>
    <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">k</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">;</span> <span class="nx">k</span><span class="o">++</span><span class="p">)</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">c</span> <span class="o">&amp;</span> <span class="mi">1</span> <span class="p">?</span> <span class="mh">0xEDB88320</span> <span class="o">^</span> <span class="p">(</span><span class="nx">c</span> <span class="o">&gt;&gt;&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">:</span> <span class="nx">c</span> <span class="o">&gt;&gt;&gt;</span> <span class="mi">1</span><span class="p">;</span>
    <span class="nx">t</span><span class="p">[</span><span class="nx">n</span><span class="p">]</span> <span class="o">=</span> <span class="nx">c</span> <span class="o">&gt;&gt;&gt;</span> <span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">t</span><span class="p">;</span>
<span class="p">})();</span>
</code></pre>
</div>
<p>For an indexed PNG you need four chunks: <code>IHDR</code> (dimensions, bit depth, colour type 3 for palette), <code>PLTE</code> (the palette), <code>tRNS</code> (per-palette-entry alpha, only if anything is transparent), and <code>IDAT</code> (the pixel data).</p>
<p>One detail that&#8217;s easy to miss: every scanline in the raw data is prefixed with a filter byte. Zero means &#8220;no filter&#8221;. Forget it and your image comes out sheared diagonally — which is at least a memorable way to find the bug.</p>
<p>Bit depth is worth packing properly too. Two colours fit in 1 bit per pixel, four in 2 bits, sixteen in 4. A two-colour image at 8 bits per pixel wastes 87% of its bytes.</p>
<h2>
<p>  Trap 3: <code>deflate</code> vs <code>deflate-raw</code><br />
</p></h2>
<p><code>IDAT</code> data is zlib-compressed, and browsers have that built in now via <code>CompressionStream</code>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="k">async</span> <span class="kd">function</span> <span class="nf">zlibDeflate</span><span class="p">(</span><span class="nx">bytes</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">cs</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CompressionStream</span><span class="p">(</span><span class="dl">'</span><span class="s1">deflate</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">writer</span> <span class="o">=</span> <span class="nx">cs</span><span class="p">.</span><span class="nx">writable</span><span class="p">.</span><span class="nf">getWriter</span><span class="p">();</span>
  <span class="nx">writer</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="nx">bytes</span><span class="p">);</span>
  <span class="nx">writer</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="k">await</span> <span class="k">new</span> <span class="nc">Response</span><span class="p">(</span><span class="nx">cs</span><span class="p">.</span><span class="nx">readable</span><span class="p">).</span><span class="nf">arrayBuffer</span><span class="p">());</span>
<span class="p">}</span>
</code></pre>
</div>
<p>The trap is the argument. <code>CompressionStream</code> accepts <code>'deflate'</code>, <code>'deflate-raw'</code> and <code>'gzip'</code>, and the naming is genuinely misleading:</p>
<ul>
<li>
<code>'deflate'</code> produces <strong>zlib</strong> format — RFC 1950, a 2-byte header plus an Adler-32 checksum</li>
<li>
<code>'deflate-raw'</code> produces <strong>raw deflate</strong> — RFC 1951, no header, no checksum</li>
</ul>
<p>PNG&#8217;s <code>IDAT</code> requires the zlib wrapper, so you want <code>'deflate'</code>. If you reach for <code>'deflate-raw'</code> because &#8220;raw deflate&#8221; sounds like what a binary format wants, you get a file that is byte-for-byte plausible and refuses to open anywhere.</p>
<h2>
<p>  The quantizer: median cut and Floyd–Steinberg<br />
</p></h2>
<p>Two classic algorithms, both about thirty lines.</p>
<p><strong>Median cut</strong> builds the palette. Put every pixel in one box. Repeatedly take the box with the widest spread, find which channel it spreads on most, sort by that channel, split at the median. Stop at your colour count. Average each box to get its palette entry.</p>
<p>It&#8217;s better than it sounds because it spends palette entries where the image actually has variation, rather than spreading them evenly across a colour space the image never visits.</p>
<p><strong>Floyd–Steinberg dithering</strong> handles the error. When a pixel&#8217;s true colour isn&#8217;t in the palette, you pick the nearest and you&#8217;re left with a difference. Throw it away and smooth gradients turn into visible bands. Instead, push that error onto the neighbours that haven&#8217;t been processed yet:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="nx">errCur</span><span class="p">[</span><span class="nx">e</span> <span class="o">+</span> <span class="mi">3</span><span class="p">]</span>  <span class="o">+=</span> <span class="nx">er</span> <span class="o">*</span> <span class="mi">7</span> <span class="o">/</span> <span class="mi">16</span><span class="p">;</span>   <span class="c1">// pixel to the right</span>
<span class="nx">errNext</span><span class="p">[</span><span class="nx">e</span> <span class="o">-</span> <span class="mi">3</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">er</span> <span class="o">*</span> <span class="mi">3</span> <span class="o">/</span> <span class="mi">16</span><span class="p">;</span>   <span class="c1">// below-left</span>
<span class="nx">errNext</span><span class="p">[</span><span class="nx">e</span><span class="p">]</span>     <span class="o">+=</span> <span class="nx">er</span> <span class="o">*</span> <span class="mi">5</span> <span class="o">/</span> <span class="mi">16</span><span class="p">;</span>   <span class="c1">// directly below</span>
<span class="nx">errNext</span><span class="p">[</span><span class="nx">e</span> <span class="o">+</span> <span class="mi">3</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">er</span> <span class="o">*</span> <span class="mi">1</span> <span class="o">/</span> <span class="mi">16</span><span class="p">;</span>   <span class="c1">// below-right</span>
</code></pre>
</div>
<p>Those four fractions are from 1976 and they still work. The error gets scattered into a fine pattern your eye averages back out, and banding disappears.</p>
<h2>
<p>  Does it actually work?<br />
</p></h2>
<p>Numbers from the test suite, on real files:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Input</th>
<th>Before</th>
<th>After</th>
<th>Saving</th>
<th>Palette</th>
</tr>
</thead>
<tbody>
<tr>
<td>Photograph</td>
<td>308,526 B</td>
<td>71,411 B</td>
<td><strong>77%</strong></td>
<td>227 colours</td>
</tr>
<tr>
<td>Flat graphic</td>
<td>2,849 B</td>
<td>2,161 B</td>
<td><strong>24%</strong></td>
<td>59 colours</td>
</tr>
</tbody>
</table>
</div>
<p>The graphic came out with a mean per-pixel difference of <strong>0.00</strong> — mathematically identical output, 24% smaller. The photograph averaged 8.96 per channel, which is invisible at normal viewing size but is a real loss, and worth being honest about: palette quantization is lossy on photographs. Photos should generally be WebP or JPEG anyway.</p>
<h2>
<p>  Trap 4: SVGs are 300×150<br />
</p></h2>
<p>Draw an SVG to a canvas and you may get a 300×150 image regardless of its <code>viewBox</code>. That&#8217;s the CSS default replaced-element size, and it applies whenever the SVG has no explicit <code>width</code> and <code>height</code>.</p>
<p>The fix is to parse the <code>viewBox</code>, strip any existing dimensions, and inject real ones before rasterising:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">vb</span> <span class="o">=</span> <span class="sr">/viewBox</span><span class="se">s</span><span class="sr">*=</span><span class="se">s</span><span class="sr">*</span><span class="se">[</span><span class="sr">"'</span><span class="se">]s</span><span class="sr">*</span><span class="se">([d</span><span class="sr">.</span><span class="se">-]</span><span class="sr">+</span><span class="se">)[s</span><span class="sr">,</span><span class="se">]</span><span class="sr">+</span><span class="se">([d</span><span class="sr">.</span><span class="se">-]</span><span class="sr">+</span><span class="se">)[s</span><span class="sr">,</span><span class="se">]</span><span class="sr">+</span><span class="se">([d</span><span class="sr">.</span><span class="se">-]</span><span class="sr">+</span><span class="se">)[s</span><span class="sr">,</span><span class="se">]</span><span class="sr">+</span><span class="se">([d</span><span class="sr">.</span><span class="se">-]</span><span class="sr">+</span><span class="se">)</span><span class="sr">/i</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="nx">tag</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">w</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">,</span> <span class="nx">h</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">;</span>
<span class="k">if </span><span class="p">(</span><span class="nx">vb</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">vw</span> <span class="o">=</span> <span class="nf">parseFloat</span><span class="p">(</span><span class="nx">vb</span><span class="p">[</span><span class="mi">3</span><span class="p">]),</span> <span class="nx">vh</span> <span class="o">=</span> <span class="nf">parseFloat</span><span class="p">(</span><span class="nx">vb</span><span class="p">[</span><span class="mi">4</span><span class="p">]);</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">vw</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">vh</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nx">w</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">;</span> <span class="nx">h</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">round</span><span class="p">(</span><span class="mi">1024</span> <span class="o">*</span> <span class="nx">vh</span> <span class="o">/</span> <span class="nx">vw</span><span class="p">);</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<h2>
<p>  Testing binary output<br />
</p></h2>
<p>Unit-testing &#8220;did the conversion work&#8221; is harder than it looks, because a file can be the right size, the right type, and still be wrong.</p>
<p>What worked: drive the real pages in headless Chromium with Playwright, then verify the <strong>bytes</strong> in Python. Pillow checks dimensions, colour mode and how many palette entries were actually used. <code>pypdf</code> checks page counts and sizes. <code>zipfile</code> verifies archive integrity. Rotation and flipping get compared pixel-wise against Pillow&#8217;s own implementations.</p>
<p>That combination caught the SVG bug, the <code>toBlob</code> fallback, and a filter-byte mistake that produced a file every viewer opened happily at the wrong dimensions.</p>
<h2>
<p>  What a browser still can&#8217;t do<br />
</p></h2>
<p>Worth stating plainly, because &#8220;it all runs client-side&#8221; can sound like a claim that it does everything:</p>
<ul>
<li>
<strong>No HEIC, RAW, PSD or TIFF decoding.</strong> Browsers can&#8217;t, and shipping a WASM decoder for each would outweigh the entire project. If you need those, a server is the right answer.</li>
<li>
<strong>Big files depend on the device.</strong> A 100-megapixel image on an old phone is slow where a server would be instant.</li>
<li>
<strong>AVIF encoding</strong> only exists where the browser supports it — which is exactly why trap 1 matters.</li>
</ul>
<p>What you get in exchange: nothing is uploaded, there&#8217;s no queue, there&#8217;s no file size limit set by someone&#8217;s bandwidth bill, and the page keeps working with the network switched off.</p>
<h2>
<p>  Code<br />
</p></h2>
<p>It&#8217;s all MIT licensed and dependency-free — one JavaScript file for the engine, one Python script that generates the site: <strong><a href="https://github.com/shaam130/ConvertPicto" rel="noopener noreferrer">github.com/shaam130/ConvertPicto</a></strong></p>
<p>The running version is at <a href="https://convertpicto.com/" rel="noopener noreferrer">convertpicto.com</a> if you want to try breaking it. I&#8217;d genuinely like to hear about the files it fails on.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/writing-a-real-png-compressor-in-vanilla-javascript-no-wasm-no-libraries.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>I Built InfoHUD Pro – Floating Desktop HUDs for Windows</title>
		<link>https://codango.com/i-built-infohud-pro-floating-desktop-huds-for-windows.htm</link>
					<comments>https://codango.com/i-built-infohud-pro-floating-desktop-huds-for-windows.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:41:48 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/i-built-infohud-pro-floating-desktop-huds-for-windows.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fvtqd3mv1ezfe06fdzop9-FWLJTE-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" />I recently released InfoHUD Pro, a Windows utility designed to keep important information visible while you work. It lets you create floating desktop HUDs for: Text · Excel · Word <a class="more-link" href="https://codango.com/i-built-infohud-pro-floating-desktop-huds-for-windows.htm">Continue reading <span class="screen-reader-text">  I Built InfoHUD Pro – Floating Desktop HUDs for Windows</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fvtqd3mv1ezfe06fdzop9-FWLJTE-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" /><p><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%2Fvtqd3mv1ezfe06fdzop9.png" class="article-body-image-wrapper"><img fetchpriority="high" decoding="async" 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%2Fvtqd3mv1ezfe06fdzop9.png" alt=" " width="800" height="843" /></a> </p>
<p>I recently released InfoHUD Pro, a Windows utility designed to keep important information visible while you work.</p>
<p>It lets you create floating desktop HUDs for:</p>
<p>Text · Excel · Word · Charts · Images · Screenshots · Web Pages · PDFs</p>
<p>You can open multiple HUDs at the same time, keep them always on top, move and resize them, lock their positions, or hide/show everything with one click.</p>
<p>It’s especially useful for dual-monitor and multi-monitor setups.</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Windows 10 / 11<br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> English / Traditional Chinese / Simplified Chinese<br />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 7-day free trial</p>
<p>Microsoft Store:<br />
<a href="https://apps.microsoft.com/detail/9nkf1rh8xck4?hl=en-US&amp;gl=US" rel="noopener noreferrer">https://apps.microsoft.com/detail/9nkf1rh8xck4?hl=en-US&amp;gl=US</a></p>
<p>I’m continuing to improve it, so feedback and suggestions are welcome. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>More products on Microsoft Store:</p>
<p><a href="https://apps.microsoft.com/search/publisher?name=AdamYang&amp;hl=en-US&amp;gl=US" rel="noopener noreferrer">https://apps.microsoft.com/search/publisher?name=AdamYang&amp;hl=en-US&amp;gl=US</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/i-built-infohud-pro-floating-desktop-huds-for-windows.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AI agents introduced several new layers to the development stack. You are probably missing this one.</title>
		<link>https://codango.com/ai-agents-introduced-several-new-layers-to-the-development-stack-you-are-probably-missing-this-one.htm</link>
					<comments>https://codango.com/ai-agents-introduced-several-new-layers-to-the-development-stack-you-are-probably-missing-this-one.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:39:19 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/ai-agents-introduced-several-new-layers-to-the-development-stack-you-are-probably-missing-this-one.htm</guid>

					<description><![CDATA[The development stack grew more in the last two years than in the previous ten. Most teams adopted the new layers so quickly they never noticed them becoming layers. Count <a class="more-link" href="https://codango.com/ai-agents-introduced-several-new-layers-to-the-development-stack-you-are-probably-missing-this-one.htm">Continue reading <span class="screen-reader-text">  AI agents introduced several new layers to the development stack. You are probably missing this one.</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>The development stack grew more in the last two years than in the previous ten. Most teams adopted the new layers so quickly they never noticed them becoming layers. Count them.</p>
<p>The agent layer: Claude Code, Codex, Cursor. The thing that writes code. Two years ago this layer did not exist; today it is where implementation happens.</p>
<p>The context layer: CLAUDE.md, AGENTS.md, rules files, memory systems. Telling agents how your repository works, what your conventions are, what not to touch. Everyone who onboarded an agent wrote these within a week, because an agent without context is a very fast junior with amnesia.</p>
<p>The tool layer: MCP. Agents reaching databases, browsers, issue trackers, deployment systems. Adopted fast because an agent that cannot touch your systems can only talk about them.</p>
<p>The isolation layer: git worktrees, containers, cloud sandboxes. One agent per checkout, because two sessions in one working directory stomp each other&#8217;s files and poison each other&#8217;s context. Anyone who ran two agents learned this in the first afternoon.</p>
<p>The orchestration layer: Gas Town, claude-squad, Foreman, and a fast-growing ecosystem of fleet runners. Who works on what, which sessions are alive, what needs review. The moment you run more than two or three agents, supervision becomes its own job, and this layer takes it.</p>
<p>Each of these layers got adopted quickly for the same reason: its absence hurts immediately and obviously. No context, the agent writes nonsense today. No isolation, files get stomped today. No orchestration, you lose track of your fleet today.</p>
<p>There is one more layer, and almost nobody runs it, because its absence hurts silently.</p>
<h2>
<p>  The failure that announces nothing<br />
</p></h2>
<p>Two agents, one repository. Agent A is told to replace PaymentService with a Stripe-specific implementation. Agent B is told to add PayPal support to PaymentService. Run the failure through every layer you have adopted.</p>
<p>The agent layer performs perfectly: both sessions write competent code. The context layer performs: both followed your conventions. The tool layer performs: every MCP call succeeded. The isolation layer performs: separate worktrees, no stomped files. The orchestration layer performs: two independent tasks, both completed, both green.</p>
<p>Git merges both branches without a conflict, because the changes touch different lines. And the design is now broken: agent B built on an extension point agent A deleted. Nothing in the toolchain objected at any moment, because every layer you have answers a different question, and none of them answer this one: can these two plans coexist?</p>
<p>Git compares diffs, and there is no diff at the moment this collision actually happens. The collision is between intentions, at plan time, before either agent has written a line. We hit versions of this for months. The one that finally made us build tooling surfaced three days after a clean merge, in production.</p>
<h2>
<p>  The coordination layer<br />
</p></h2>
<p>The missing layer answers exactly one question: can the work currently in flight all be true at once?</p>
<p>To be precise about what it does not do: it does not create plans. Your agents plan however they already plan. The coordination layer is a ledger and a referee, not a planner. Each agent declares what it is about to change before writing code: a semantic scope plus an operation, like symbol:PaymentService=replace. Scopes are not file paths, because file paths miss API, schema, configuration, and cross-language collisions. Deterministic rules compare the declarations and raise a finding while both sides are still plans: the rule that fired, an explanation, a suggested resolution. In the PaymentService case, one destructive operation and one additive operation on the same scope cannot both be true, and the second agent learns this in the same call that published its intent.</p>
<p>The other half of the layer is evidence. When work finishes, acceptance is gated on a verification command the layer executes itself against the exact fingerprint of the candidate change. An agent reporting that tests pass is provenance, not proof. If the tree changed after validation, the attempt does not count.</p>
<h2>
<p>  What building one taught us<br />
</p></h2>
<p>Three lessons, each paid for.</p>
<p>First: declared beats inferred. We started by inferring operations from agent prose, and it produced confident false alarms. &#8220;Delete the flaky ThumbnailCache benchmark test&#8221; was read as destroying ThumbnailCache itself. Widening the verb list just moved the boundary. Making agents declare the operation as structured syntax removed the whole error class.</p>
<p>Second: advisory beats locks, but severity must be earned. Most overlaps between agents are compatible, and a lock serializes work that did not need serializing; the moment a lock exists, a busy repo becomes a queue. So claims warn instead of block. The discipline that makes warnings survivable is reserving HIGH for two declarations that genuinely cannot both be true. Our repo carries a regression fixture that fails the build if compatible work ever raises a HIGH.</p>
<p>Third: soft claims, hard gate. Everything before the finish line is advisory, because the finish line is not: no acceptance while an unresolved HIGH finding stands, and no acceptance on self-reported evidence. The gate staying hard is what lets everything before it stay soft.</p>
<h2>
<p>  Do you need this layer?<br />
</p></h2>
<p>If you run one coding agent, no. Close the tab, you are fine.</p>
<p>If you run two or more on the same repository, whether in parallel today or sequentially across days, you have already had the silent version of this failure whether you noticed it or not. The tell: a change that made you say &#8220;wait, when did that class stop existing?&#8221; three days after a clean merge.</p>
<p>We open-sourced the coordination layer we built and use: Foremerge (<a href="https://github.com/naw103/foremerge" rel="noopener noreferrer">https://github.com/naw103/foremerge</a>), Apache-2.0, one Rust binary, local-first, above Git rather than inside it. Setup is one paste into Claude Code, Codex, or Cursor. Stated plainly per the honesty its own docs demand: detection is deterministic but heuristic and can warn on compatible work, claims never lock anything, and there are no published benchmarks yet, so no performance claims. The layer matters regardless of whose implementation you run, including one you build yourself.</p>
<p>The stack grew five layers in two years because each one&#8217;s absence hurt the day you needed it. The sixth one&#8217;s absence hurts three days later. That is the only reason you are probably missing it.</p>
<p>Foremerge<br />
The open-source coordination protocol for parallel coding agents, built above Git. Agents declare intent and semantic scopes before writing code, so plans that cannot both be true collide in a queryable store instead of in your merge. One Rust binary, Apache-2.0, local-first.</p>
<p><code>curl -fsSL https://foremerge.com/install.sh | sh</code></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/ai-agents-introduced-several-new-layers-to-the-development-stack-you-are-probably-missing-this-one.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Auto-Revoke a Claude Agent&#8217;s Access When a User Is Offboarded With Kinde Webhooks</title>
		<link>https://codango.com/how-to-auto-revoke-a-claude-agents-access-when-a-user-is-offboarded-with-kinde-webhooks.htm</link>
					<comments>https://codango.com/how-to-auto-revoke-a-claude-agents-access-when-a-user-is-offboarded-with-kinde-webhooks.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:36:32 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-to-auto-revoke-a-claude-agents-access-when-a-user-is-offboarded-with-kinde-webhooks.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fyfgm7hwfnspmmqcvfozx-oJCzxd-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />Imagine this scenario, someone on your team gets offboarded while their AI agent is still mid-task. Nobody remembers the agent is even running. It&#8217;s just doing what it was told, <a class="more-link" href="https://codango.com/how-to-auto-revoke-a-claude-agents-access-when-a-user-is-offboarded-with-kinde-webhooks.htm">Continue reading <span class="screen-reader-text">  How to Auto-Revoke a Claude Agent&#8217;s Access When a User Is Offboarded With Kinde Webhooks</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fyfgm7hwfnspmmqcvfozx-oJCzxd-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>Imagine this scenario, someone on your team gets offboarded while their AI agent is still mid-task. Nobody remembers the agent is even running. It&#8217;s just doing what it was told, on behalf of someone who, as of a minute ago, doesn&#8217;t work at your company or on your team anymore.</p>
<p>Does it stop?</p>
<p>Well, I built a small app to test that scenario, using <a href="https://kinde.com/?utm_source=devto&amp;utm_medium=content&amp;utm_campaign=shola&amp;campaignid=chatgptapp&amp;network=&amp;adgroup=&amp;keyword=&amp;matchtype=&amp;creative=24&amp;device=&amp;adposition=" rel="noopener noreferrer">Kinde</a> to handle sign-in and to hold the record of who&#8217;s still active. I signed a real user in, handed their agent a task, and while the agent was still working through it, suspended that same user, in Kinde&#8217;s dashboard, mid-run, to see what the agent would do next.</p>
<p>Unsurprisingly, the agent kept working on the task.</p>
<p>You see, suspending or deleting a person changes how Kinde itself sees that user, but it doesn&#8217;t touch the access token their agent is already holding, because nothing about a suspension reaches back into a token that was already signed and handed out before it happened. The token still verifies exactly as it did before the suspension, so the agent has no way to know anything changed.</p>
<p>Everything I am going to talk about in this article is about closing that gap, and about what I actually found while doing it: webhook deliveries measured live, a production bug that could have left an offboarded user&#8217;s record looking active forever, and a hard number for how long an offboarded person&#8217;s agent keeps acting before anything catches it.</p>
<h2>
<p>  Why a suspended user&#8217;s token still works<br />
</p></h2>
<p>Let&#8217;s start with what an access token actually is, because the whole gap follows from it. An OAuth access token isn&#8217;t a receipt you hand back to check against a ledger. It&#8217;s a signed claim, a small JSON payload with a cryptographic signature attached, and whatever&#8217;s checking it just verifies that signature against a public key rather than calling home to ask if the token&#8217;s still good. That&#8217;s the entire appeal of the design: an API can confirm a token is genuine without a database round trip on every request.</p>
<p>Which means suspending a user in Kinde only changes a row in Kinde&#8217;s own database. It doesn&#8217;t reach the token at all, because there&#8217;s nothing there for it to reach: the token was already handed out, already signed, already valid until whatever expiry it was minted with. Revoking it properly would mean tracking every issued token in a lookup table somewhere, which throws away the entire point of signing one in the first place, or it would mean just waiting for the thing to expire on its own.</p>
<p>I suspended a signed-in test user mid-session, and the app&#8217;s own check kept reporting that user&#8217;s access token as valid, seconds after Kinde had already suspended them.</p>
<p>So that gap isn&#8217;t a bug in Kinde, and it isn&#8217;t a bug in OAuth either. It&#8217;s just what a stateless credential is, by design, and the real question is what you build on top of it.</p>
<h2>
<p>  The shape of the fix<br />
</p></h2>
<p>Two pieces close the gap. A webhook tells the app when Kinde&#8217;s view of a user changes, and a check runs before every single agent action, reading the app&#8217;s own record of that user instead of trusting whatever was true when the session started.</p>
<p><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%2Fyfgm7hwfnspmmqcvfozx.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fyfgm7hwfnspmmqcvfozx.png" alt="A flowchart showing how Kinde user suspensions propagate through signed webhooks and reconciliation into Convex, with an enforcement seam controlling Claude agent tool calls." width="800" height="518" /></a></p>
<p>The agent itself is a small Claude Messages API loop, working through a closed set of three tools against a demo set of internal resources: <code>list_resources</code>, <code>read_resource</code>, <code>write_resource</code>. None of what follows is specific to what the agent does. It&#8217;s specific to the one place every tool call has to pass through before it&#8217;s allowed to run at all.</p>
<h2>
<p>  Building the seam<br />
</p></h2>
<p>First, we start with the registry that defines those three actions, because it&#8217;s closed by construction rather than by convention. An action that isn&#8217;t in this table doesn&#8217;t half-exist somewhere in the code, waiting to be called by accident. It just doesn&#8217;t exist:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">ACTION_REGISTRY</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="nx">ActionName</span><span class="p">,</span> <span class="nx">ActionDefinition</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">list_resources</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">list_resources</span><span class="dl">"</span><span class="p">,</span> <span class="na">destructive</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">params</span><span class="p">:</span> <span class="p">{}</span> <span class="p">},</span>
  <span class="na">read_resource</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">read_resource</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">destructive</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">resourceId</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">},</span>
  <span class="p">},</span>
  <span class="na">write_resource</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">write_resource</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">destructive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">params</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">resourceId</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span>
      <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">};</span>
</code></pre>
</div>
<p>Both the tool schema handed to Claude and the enforcement check are built from this same table, so the two can never quietly drift apart from each other the way a schema and a permissions list usually do once someone forgets to update one of them.</p>
<p>Every tool call the model makes passes through a single function, <code>enforceToolCall</code>, which looks up the acting user&#8217;s current status and hands it to a small, pure decision function underneath it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">function</span> <span class="nf">decideAccess</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">mode</span><span class="p">:</span> <span class="nx">EnforcementMode</span><span class="p">;</span>
  <span class="nl">userStatus</span><span class="p">:</span> <span class="nx">UserStatus</span><span class="p">;</span>
<span class="p">}):</span> <span class="p">{</span> <span class="nl">decision</span><span class="p">:</span> <span class="nx">SeamDecision</span><span class="p">;</span> <span class="nl">reason</span><span class="p">:</span> <span class="nx">SeamReason</span> <span class="p">}</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">mode</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">naive</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">decision</span><span class="p">:</span> <span class="dl">"</span><span class="s2">allow</span><span class="dl">"</span><span class="p">,</span> <span class="na">reason</span><span class="p">:</span> <span class="dl">"</span><span class="s2">naive_mode_no_check</span><span class="dl">"</span> <span class="p">};</span>
  <span class="p">}</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">userStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">decision</span><span class="p">:</span> <span class="dl">"</span><span class="s2">allow</span><span class="dl">"</span><span class="p">,</span> <span class="na">reason</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user_active</span><span class="dl">"</span> <span class="p">};</span>
  <span class="p">}</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">userStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">offboarded</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">decision</span><span class="p">:</span> <span class="dl">"</span><span class="s2">refuse</span><span class="dl">"</span><span class="p">,</span> <span class="na">reason</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user_offboarded</span><span class="dl">"</span> <span class="p">};</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="p">{</span> <span class="na">decision</span><span class="p">:</span> <span class="dl">"</span><span class="s2">refuse</span><span class="dl">"</span><span class="p">,</span> <span class="na">reason</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user_unknown</span><span class="dl">"</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Naive mode allows every call without ever looking at that status, which is the vulnerability this whole piece is about, reproduced on purpose so both modes can run side by side against the exact same code and prove the point cleanly. Enforced mode is stricter in a way that matters: it allows exactly one case, a confirmed active user, and refuses everything else, including a status the seam couldn&#8217;t even resolve because a read to Convex failed. An unknown status doesn&#8217;t get the benefit of the doubt.</p>
<p>That status comes from a webhook. <a href="https://kinde.com/?utm_source=devto&amp;utm_medium=content&amp;utm_campaign=shola&amp;campaignid=chatgptapp&amp;network=&amp;adgroup=&amp;keyword=&amp;matchtype=&amp;creative=24&amp;device=&amp;adposition=" rel="noopener noreferrer">Kinde</a> sends a signed event on <code>user.updated</code> and <code>user.deleted</code>, the receiver verifies the signature, and then it does one more thing that has nothing to do with the signature at all:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">function</span> <span class="nf">isFreshWebhookEvent</span><span class="p">(</span>
  <span class="nx">event</span><span class="p">:</span> <span class="nx">WebhookEvent</span><span class="p">,</span>
  <span class="nx">now</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">(),</span>
<span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">eventTime</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">);</span>
  <span class="k">if </span><span class="p">(</span><span class="nb">Number</span><span class="p">.</span><span class="nf">isNaN</span><span class="p">(</span><span class="nx">eventTime</span><span class="p">))</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
  <span class="k">return</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">abs</span><span class="p">(</span><span class="nx">now</span> <span class="o">-</span> <span class="nx">eventTime</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="nx">MAX_CLOCK_SKEW_MS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>A valid signature only proves Kinde signed this payload at some point. It says nothing about when that was, so without this check, a captured event replayed months later would sail straight past signature verification, since the signature itself never expires, and past deduplication too, because dedup only catches an event id it&#8217;s already seen before. <code>MAX_CLOCK_SKEW_MS</code> is five minutes. Anything older than that gets rejected the same way a forged signature would.</p>
<p><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%2Ft1jum6ormuldmfnsz8q5.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Ft1jum6ormuldmfnsz8q5.png" alt="Kinde's suspend and restore admin action panel" width="799" height="347" /></a></p>
<h2>
<p>  The bug the build actually found<br />
</p></h2>
<p>Hardening this receiver turned up a real ordering bug. The first version recorded the webhook&#8217;s delivery, for deduplication, before it applied the actual effect of marking the user offboarded. That ordering has a quiet failure mode: if the effect write failed right after the delivery had already been logged, a retried webhook would look like a duplicate of one already handled and get skipped. The user would never actually get offboarded, and nothing about the system would ever try again.</p>
<p>The fix took one line of reordering, but it only works because of one property underneath it: the effect, <code>markOffboarded</code>, is idempotent, so running it twice is always safe. That&#8217;s why it now runs first, unconditionally, ahead of the bookkeeping whose entire job is to stop it from running a third or fourth time. Recording the delivery first and applying the effect second felt like the more natural order to write. It was also the less safe one.</p>
<h2>
<p>  Proving it, live<br />
</p></h2>
<p><code>scripts/e2e-narrative.ts</code> runs one task against one real Kinde test user, twice: once with the seam in naive mode, once enforced, suspending that same user for real, mid-run, both times. Nothing in this script is simulated. It drives a real agent loop, fires a real suspend call at Kinde, and waits on the actual webhook to arrive over a tunnel before it checks what actually happened.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th></th>
<th>naive</th>
<th>enforced</th>
</tr>
</thead>
<tbody>
<tr>
<td>actions allowed after offboarding</td>
<td>2</td>
<td>0</td>
</tr>
<tr>
<td>where the run stopped</td>
<td>it didn&#8217;t, ran to completion</td>
<td>step 2, reason <code>user_offboarded</code>
</td>
</tr>
</tbody>
</table>
</div>
<p>The same story shows up in the operator console, so I ran it once more while writing this, offboarding the signed-in user on purpose partway through a task:</p>
<p><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%2Fw9eg7wjseb5g6wpn2h52.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fw9eg7wjseb5g6wpn2h52.png" alt="The console's live timeline: step 1 allowed while active, step 2 refused after offboarding" width="800" height="628" /></a></p>
<p>Step one lands while the user&#8217;s still active. The offboard request goes out. Step two refuses, <code>user_offboarded</code>, with a cutoff latency of 2697ms measured from when the offboarding itself landed in the database, not from when the run started. Every one of those decisions lands in an audit log under a shared correlation id, so a run&#8217;s full timeline can be pulled back up after the fact, not just watched live:</p>
<p><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%2Fe7e5wlkul9jkvtma03is.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fe7e5wlkul9jkvtma03is.png" alt="Convex's auditLog table, showing real seam decisions and webhook deliveries" width="799" height="254" /></a></p>
<h2>
<p>  The numbers that matter more than the demo<br />
</p></h2>
<p>Across every live webhook delivery in this build, latency ran from 652ms to 2058ms, across suspends, restores, deletes, and role changes alike. That&#8217;s not instant. Most people assume it is.</p>
<p>The enforcement check itself doesn&#8217;t belong anywhere in that number. It&#8217;s one indexed read against Convex, and next to webhook delivery it&#8217;s close enough to free that it doesn&#8217;t move the total.</p>
<p>So the more honest way to describe total revocation speed is this: it&#8217;s webhook delivery latency, plus however long until the agent gets around to its next real action, and that second part isn&#8217;t a fixed system number at all. An agent moving faster, with no artificial pacing between steps or several tool calls requested in the same turn, gets caught just as fast on its very next call, without any extra mechanism needed to catch it sooner. The floor here is roughly one model round trip, not the enforcement check sitting underneath it.</p>
<p>A webhook can also be missed or delayed, because that&#8217;s what &#8220;best-effort delivery&#8221; actually means in practice, so I built a reconciliation sweep on top: a cron job checking every active user&#8217;s live status directly against Kinde every five minutes. I tested it against a forced scenario: suspended a real user, then manually pushed the app&#8217;s own record back to active, simulating a webhook that never arrived at all. The cron caught the drift and corrected it on its own, before I ever triggered a manual run.</p>
<h2>
<p>  Here&#8217;s what I think<br />
</p></h2>
<p>&#8220;Revocation&#8221; is the wrong word for what any of this does, and I think that matters more than it sounds like it should. Nothing here revokes anything. What actually happens is a live check gets bolted on top of a credential that was never revocable to begin with, and I&#8217;d rather describe it that way than pretend otherwise, because that&#8217;s closer to what every system in this space is doing under the hood, whatever the marketing copy on top of it calls it.</p>
<p>I&#8217;d also argue this check shouldn&#8217;t be the thing you bolt on during a hardening pass once the demo already works, which is exactly the order it happened in during this build. The seam existed early on, but the reconciliation backstop and the timestamp check both came later, as production polish, and I think that ordering is backwards for anything that runs unattended. A browser session gets re-validated more or less by accident, on every page load, because there&#8217;s a human sitting there generating new requests the whole time. An agent loop doesn&#8217;t get that for free. It holds one credential, validated once at the start, and then it acts on that credential in a loop nobody&#8217;s watching in real time. If the check isn&#8217;t built into that loop from the first line, there&#8217;s no accident later that adds it back in.</p>
<p>The usual objection to this is cost: a database read on every single tool call sounds expensive once an agent&#8217;s task means dozens of calls instead of one. But this build&#8217;s own numbers say that objection doesn&#8217;t hold up, at least not with an indexed lookup like the one here. The check disappears next to webhook latency, and webhook latency itself disappears next to how long a person actually takes to notice someone&#8217;s gone and go click suspend. The genuinely expensive part of this whole system is the five to fifteen minutes between someone walking out the door and someone else noticing. Nobody optimizes for that number, even though it&#8217;s the one that actually decides how exposed you are.</p>
<h2>
<p>  What this doesn&#8217;t solve<br />
</p></h2>
<p>The token-still-valid gap from the top of this piece is real, and nothing here closes it. This build works around it, by checking liveness on every call, instead of trying to make the token itself stop working. A system that actually needs the token revoked needs a different mechanism entirely, like short-lived tokens or an introspection endpoint hit on every use, and both of those trade away the exact stateless-verification benefit that made a signed token worth using in the first place.</p>
<p>The reconciliation sweep is a five-minute backstop, not the primary path, and if the webhook&#8217;s doing its job, the sweep never finds anything to correct.</p>
<p><code>runs.timeline</code>, <code>runs.get</code>, and the audit queries in this build are also unauthenticated Convex reads. A run id or a correlation id is enough on its own to read that run&#8217;s whole timeline, which is fine for a single-user demo console and not fine at all for anything with untrusted users in it.</p>
<p>And the action registry here covers exactly three read and write actions on a demo resource. That&#8217;s enough to prove the pattern holds. It&#8217;s nowhere near enough to prove the pattern scales to a real authorization model with real permission boundaries between real resources.</p>
<h2>
<p>  Where this leaves the agent<br />
</p></h2>
<p>Back to the opening scene: someone gets offboarded, and their agent is mid-task. Whether its next few tool calls go through was never actually a question about the token. The token was always going to keep working right up until it expired on its own, offboarding or not, because that&#8217;s just what it is. The real question was always whether anything standing between the agent and the action it&#8217;s about to take bothered to check, right then, whether the person behind it was still around.</p>
<h2>
<p>  Code and sources<br />
</p></h2>
<p>The full build, the enforcement seam, the webhook handler, the reconciliation cron, and <code>scripts/e2e-narrative.ts</code>, is on GitHub: <a href="https://github.com/sholajegede/offboarding-revocation-demo" rel="noopener noreferrer">sholajegede/offboarding-revocation-demo</a>.</p>
<p>The webhook signing and delivery model comes from Kinde&#8217;s own <a href="https://docs.kinde.com/integrate/webhooks/about-webhooks/" rel="noopener noreferrer">webhooks documentation</a>, and the suspend and restore actions used throughout the demo follow Kinde&#8217;s <a href="https://docs.kinde.com/kinde-apis/management/" rel="noopener noreferrer">Management API</a> and its notes on <a href="https://docs.kinde.com/manage-users/access-control/delete-or-suspend-users/" rel="noopener noreferrer">suspending and deleting users</a>.</p>
<p>The reconciliation sweep runs on Convex&#8217;s <a href="https://docs.convex.dev/scheduling/cron-jobs" rel="noopener noreferrer">cron jobs</a>, and the agent loop follows Anthropic&#8217;s <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" rel="noopener noreferrer">tool use documentation</a> for the Messages API. Every number in this piece came from <code>runs.timeline</code> and <code>auditLog</code> in that same Convex deployment, read straight off the live runs.</p>
<p>Clone it, wire it up against <a href="https://kinde.com/?utm_source=devto&amp;utm_medium=content&amp;utm_campaign=shola&amp;campaignid=chatgptapp&amp;network=&amp;adgroup=&amp;keyword=&amp;matchtype=&amp;creative=24&amp;device=&amp;adposition=" rel="noopener noreferrer">your own Kinde</a> tenant, and offboard a test user mid-run. Your webhook latency might not match mine, your reconciliation sweep could catch different drift, and that&#8217;s the point really. Drop your numbers in the comments and let me know what you find.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-to-auto-revoke-a-claude-agents-access-when-a-user-is-offboarded-with-kinde-webhooks.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>codsh 0.23.0 — Mission Contract: `/ship` that won&#8217;t silently drift</title>
		<link>https://codango.com/codsh-0-23-0-mission-contract-ship-that-wont-silently-drift.htm</link>
					<comments>https://codango.com/codsh-0-23-0-mission-contract-ship-that-wont-silently-drift.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 01:23:56 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/codsh-0-23-0-mission-contract-ship-that-wont-silently-drift.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F72mq5vbhqo7cbhubcthw-6ooEoI-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />codsh is a DeepSeek-native terminal coding agent on open dsh. It is not a Claude Code env wrapper. The hook stays the same: /ship takes one sentence to verified code. <a class="more-link" href="https://codango.com/codsh-0-23-0-mission-contract-ship-that-wont-silently-drift.htm">Continue reading <span class="screen-reader-text">  codsh 0.23.0 — Mission Contract: `/ship` that won&#8217;t silently drift</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F72mq5vbhqo7cbhubcthw-6ooEoI-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p><strong>codsh</strong> is a DeepSeek-native terminal coding agent on open <a href="https://github.com/deepseek-ai/deepseek-harness" rel="noopener noreferrer">dsh</a>. It is <strong>not</strong> a Claude Code env wrapper.</p>
<p>The hook stays the same: <strong><code>/ship</code> takes one sentence to verified code.</strong></p>
<p><strong>0.23.0</strong> ships the <strong>Mission Contract</strong> control plane for that loop — so acceptance criteria stay sealed, drift blocks the run instead of rewriting the goal, and delivery needs recorded evidence.</p>
<h2>
<p>  What changed in 0.23.0<br />
</p></h2>
<p>When Gate 1 confirms the spec, <code>/ship</code> compiles a machine-checkable <strong>Mission Contract</strong> (<code>mission.contract.json</code>) from the sealed Main Track, Out of Scope, and acceptance criteria.</p>
<p>After seal:</p>
<ul>
<li>
<strong>Immutable</strong> — Main Track, Out of Scope, grill decisions, original wording, acceptance criteria, and the contract JSON. Protected writes are refused; external drift stops the run and stays on disk for inspection.</li>
<li>
<strong>Alignment Gate</strong> — refuses writes that lack requirement mapping or hit immutable memory.</li>
<li>
<strong>Drift Detector</strong> — scores plan/action drift against the seal; a rewritten Main Track is a <strong>blocker</strong>, not an accepted rewrite.</li>
<li>
<strong>Verifier</strong> — matches acceptance criteria to <strong>recorded evidence</strong>. Delivery does not complete without it.</li>
</ul>
<p>Also in this release: <strong>wayfinder</strong> before the grill interview (destination + open decisions, separate from implementation), and tighter parent/subagent coordination — one parent-owned ticket per turn, fresh-context children for investigation / landing / review, parent keeps questions, gates, and final proofs.</p>
<p>npm: <code>codsh-cli</code> + <code>codsh-bundle</code> <strong>0.23.0</strong>.</p>
<h2>
<p>  Why this matters vs “just chat a plan”<br />
</p></h2>
<p>Ad-hoc agent plans live in conversation memory. They drift. Acceptance becomes vibes.</p>
<p><code>/ship</code> keeps the human projection in Markdown and the runner-owned contract on disk:</p>
<ol>
<li>Pre-flight → wayfinder → grill
</li>
<li>Spec (Gate 1) → Tickets (Gate 2)
</li>
<li>Landing (TDD) → evidence-based Done
</li>
</ol>
<p>While work is in flight you get a <strong>panorama</strong>: TTY overlay (<code>Ctrl+G</code>), a ticket-count teaser with the local Web flowchart URL, and the flowchart itself — <code>/ship</code> does not auto-open a browser.</p>
<p>Coming from Claude Code / Codex? Different harness, different product. Redirecting Claude Code at DeepSeek with env vars works; this is the native path on dsh. Map what maps / what’s missing on <a href="https://github.com/Blackman99/codsh/issues/64" rel="noopener noreferrer">issue #64</a>.</p>
<h2>
<p>  What <code>/ship</code> builds (gallery)<br />
</p></h2>
<p>Every gallery project started from a <strong>one-sentence</strong> request. Real screenshots and playable results: <a href="https://blackman99.github.io/codsh/gallery.html" rel="noopener noreferrer">blackman99.github.io/codsh/gallery.html</a></p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72mq5vbhqo7cbhubcthw.webp" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72mq5vbhqo7cbhubcthw.webp" alt="Medal of Honor — WWII browser game from one  raw `/ship` endraw  sentence" width="800" height="500" /></a></p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqz48gwc8iwvbtifwhck.webp" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwqz48gwc8iwvbtifwhck.webp" alt="International mall —  raw `/ship` endraw  gallery still" width="800" height="500" /></a></p>
<p><em>(Publishing: upload local PNGs from <code>/workspace/codsh-assets/gallery-0.23.0/</code> as cover + inline if Dev.to prefers hosted uploads.)</em></p>
<h2>
<p>  Install (60 seconds)<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npm <span class="nb">install</span> <span class="nt">-g</span> @deepseek-ai/dsh codsh-cli
<span class="nb">export </span><span class="nv">DEEPSEEK_API_KEY</span><span class="o">=</span>…   <span class="c"># or any OpenAI-compatible route via ~/.dsh/settings.yaml</span>
codsh
</code></pre>
</div>
<p>Node <code>≥22.19</code>. Already on a matching dsh? <code>npm i -g codsh-cli</code> is enough.</p>
<p>Then:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>/ship let long diffs open in a pager instead of scrolling past
</code></pre>
</div>
<p>For tiny edits, plain chat or <code>⇧Tab</code> plan mode is enough — <code>/ship</code> is multi-phase and burns tokens on purpose.</p>
<h2>
<p>  Links<br />
</p></h2>
<ul>
<li>Repo: <a href="https://github.com/Blackman99/codsh" rel="noopener noreferrer">https://github.com/Blackman99/codsh</a>
</li>
<li>Site (real terminal captures): <a href="https://blackman99.github.io/codsh/" rel="noopener noreferrer">https://blackman99.github.io/codsh/</a>
</li>
<li>Gallery: <a href="https://blackman99.github.io/codsh/gallery.html" rel="noopener noreferrer">https://blackman99.github.io/codsh/gallery.html</a>
</li>
<li>npm: <a href="https://www.npmjs.com/package/codsh-cli" rel="noopener noreferrer"><code>codsh-cli</code></a> · <a href="https://www.npmjs.com/package/codsh-bundle" rel="noopener noreferrer"><code>codsh-bundle</code></a> (MIT)
</li>
<li>Claude Code / Codex migrants: <a href="https://github.com/Blackman99/codsh/issues/64" rel="noopener noreferrer">https://github.com/Blackman99/codsh/issues/64</a>
</li>
</ul>
<p>Early project. Sharp edges welcome — especially Windows (#62) and third-party endpoints (#63).</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/codsh-0-23-0-mission-contract-ship-that-wont-silently-drift.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>I Made the Top Model My Session Default, and One Heavy Implementation Plus Its Review Burned Through Fable&#8217;s Usage Limit</title>
		<link>https://codango.com/i-made-the-top-model-my-session-default-and-one-heavy-implementation-plus-its-review-burned-through-fables-usage-limit.htm</link>
					<comments>https://codango.com/i-made-the-top-model-my-session-default-and-one-heavy-implementation-plus-its-review-burned-through-fables-usage-limit.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 12:12:21 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/i-made-the-top-model-my-session-default-and-one-heavy-implementation-plus-its-review-burned-through-fables-usage-limit.htm</guid>

					<description><![CDATA[On August 22, 2026, I started a heavy implementation in a Fable session and ran a review right after it. Fable&#8217;s usage limit was gone in an instant. That happened <a class="more-link" href="https://codango.com/i-made-the-top-model-my-session-default-and-one-heavy-implementation-plus-its-review-burned-through-fables-usage-limit.htm">Continue reading <span class="screen-reader-text">  I Made the Top Model My Session Default, and One Heavy Implementation Plus Its Review Burned Through Fable&#8217;s Usage Limit</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>On August 22, 2026, I started a heavy implementation in a Fable session and ran a review right after it. Fable&#8217;s usage limit was gone in an instant. That happened because the built-in skill that handles the review inherited the session&#8217;s model and ran on it.</p>
<p>Three days later, in the morning, I was chasing a different problem and noticed something. The over-engineering that shows up in my own repositories was happening in the Opus sessions — the ones that picked up design work after Fable&#8217;s usage limit ran out.</p>
<p>Those two are the first and second half of the same event. When you make the top model your session default, the first thing that breaks is not the work product but the usage limit, and once the limit is gone, judgment itself drops to a lower model. This article is a record of which paths the default leaked into, why a convention and a warning failed to stop it, what did stop it, and what fell once the limit was gone.</p>
<h2>
<p>  Setup: three models across three tiers<br />
</p></h2>
<p>I use Claude Code on a subscription plan (Max) and split three models by role. Fable, the top model, does judgment — checking premises, planning, acceptance. Below it, Opus does implementation, and I hand that work to a new session. Mechanical cross-checking goes to Sonnet. On my plan, Fable&#8217;s usage limit is separate from Opus&#8217;s, so Opus still works after Fable runs out. From here on I call the judgment side the judge tier and the implementation side the build tier (in my config files they are literally <code>judge-tier</code> and <code>build-tier</code>). My <code>settings.json</code> default model is <code>fable</code>, and I wrote up the division of labor for handing implementation off in <a href="https://dev.to/shimo4228/i-handed-41-tasks-to-an-ai-loop-the-bottleneck-was-judgment-not-code-23dp">an earlier article</a>.</p>
<p>Since the start of August, every subagent I wrote myself carries a <code>model:</code> line. Writing the model into the definition file to fix it is what I call pinning below. The 11 files in <code>~/.claude/agents/*.md</code> break down as fable 1 / opus 4 / sonnet 5 / haiku 1 as of September 16, the day I started writing this. A lint rejects any I forget.</p>
<p>I thought that was enough to have the tiers in place.</p>
<h2>
<p>  The default leaks into paths you cannot pin<br />
</p></h2>
<p>What leaked on the day the limit ran out were <code>/code-review</code> and <code>/simplify</code>. Both ship with Claude Code, take no model argument, and run on the session&#8217;s model. Call them from a Fable session and they run on Fable. That is how the limit arrived in an instant.</p>
<p>There are other leak paths: the built-in subagents, which have no frontmatter. Counting my own log (<code>~/.claude/metrics/agent-usage.jsonl</code>), over the three weeks from installing the review hook described below to writing this article, 576 agent launches included 373 built-ins — general-purpose 231, Explore 121, Plan 10, claude-code-guide 11 — or 65%. Writing <code>model:</code> into my own agents does not reach two thirds of the launches. (claude-code-guide is fixed to Haiku, so the count that can inherit the session model is 362, or 63%.) The log does not record the model, so I cannot say how many of those ran on Fable. It was a hole I only noticed by counting, and I closed most of it while writing this article. What I closed it with is at the end.</p>
<p>Per the official documentation (as of September 16, 2026), a subagent&#8217;s model is resolved in this order: the model named at call time, the <code>model:</code> in the definition, the <code>CLAUDE_CODE_SUBAGENT_MODEL</code> environment variable, then the session&#8217;s model. Explore inherits the session model but caps at Opus; general-purpose and Plan inherit with no cap. So if the caller names no model and the environment variable is unset, every general-purpose a Fable session launches is Fable. Setting the variable changes general-purpose only; Explore and Plan do not move.</p>
<p>I fell into the same mechanism back in February. At the time, <code>--model opus</code> applied only to the main loop, and my subagents ran on Haiku, which I had not intended (<a href="https://dev.to/shimo4228/i-tried-an-opus-orchestrator-and-killed-it-the-roi-of-multi-agent-systems-1f7g">the article from back then</a>; the behavior has changed since). In February it leaked below what I intended, this time above it. Inheritance defaults leak in both directions.</p>
<p>The same report shows up in public issues. #76514, from July 10, says that omitting the per-agent <code>model</code> propagates Fable to every subagent; it was closed as not planned. #93894, from September 12, says that one run of <code>/code-review high</code> on Fable 5.1 exhausts the session budget and the review never finishes; it is still open.</p>
<h2>
<p>  The convention did not stop it, and neither did the warning<br />
</p></h2>
<p>My fix on the day the limit ran out was a convention. I added a step at the end of every implementation plan — decide in one line whether this session implements — and made heavy implementation go to a new Opus session. I rejected enforcing it with a hook, because that would bury the policy inside the hook.</p>
<p>Two days later, at night, I typed this (my own words, translated):</p>
<blockquote>
<p>&#8230;I put in a convention where Fable handles design and opus does the implementation, but it isn&#8217;t really being followed. Implementation is one thing, but when review runs straight afterwards — simplify and code-review especially — it wastes a serious amount of tokens&#8230;.</p>
</blockquote>
<p>So I added a hook. The first version was an advisory: warn when a review is launched in a Fable session. What I learned the next morning is that an advisory gets read <em>after</em> the review has already run. Seeing the warning, stopping, and re-running the review on Opus means paying twice — once for Fable, once for Opus.</p>
<p>That same morning I rewrote <code>review-model-notice.sh</code>. What I had rejected first was burying policy in a hook. What went into the hook this time is only a mechanical test — skill name crossed with session model — while the policy of which model does what stays on the rules side. The response is split by path.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># ~/.claude/hooks/review-model-notice.sh (excerpt, abridged)</span>
<span class="c"># Split the response by path:</span>
<span class="c">#   Direct skill call (code-review / simplify) -&gt; **block**. The test is purely</span>
<span class="c">#     mechanical (skill name x session model) with no room for a false positive,</span>
<span class="c">#     and as an advisory the skill runs in the same turn and burns judge-tier</span>
<span class="c">#     tokens before the advice is ever read (measured — stopping and re-running</span>
<span class="c">#     the review on Opus meant paying twice). Only a block before execution works.</span>
<span class="c">#   Agent/Task launch with a missing model pin -&gt; stay at advisory. Partial</span>
<span class="c">#     prompt matching is a heuristic and can produce false positives, so it does</span>
<span class="c">#     not get deny authority.</span>
</code></pre>
</div>
<p>The session&#8217;s model is not in the hook payload. It reads the most recent <code>"model"</code> from the tail of the transcript, and stays silent if it cannot read one.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="nv">model</span><span class="o">=</span><span class="si">$(</span><span class="nb">tail</span> <span class="nt">-c</span> 2000000 <span class="s2">"</span><span class="nv">$T</span><span class="s2">"</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-o</span> <span class="s1">'"model" *: *"[^"]*"'</span> | <span class="nb">tail</span> <span class="nt">-n</span> 1<span class="si">)</span> <span class="o">||</span> <span class="nb">true
</span><span class="k">case</span> <span class="s2">"</span><span class="nv">$model</span><span class="s2">"</span> <span class="k">in</span>
  <span class="k">*</span>fable<span class="k">*</span><span class="p">)</span> <span class="p">;;</span>
  <span class="k">*</span><span class="p">)</span> <span class="nb">exit </span>0 <span class="p">;;</span>
<span class="k">esac</span>
<span class="c"># ...(compose the block / advisory text)</span>
<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$mode</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"block"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span>jq <span class="nt">-cn</span> <span class="nt">--arg</span> reason <span class="s2">"</span><span class="nv">$msg</span><span class="s2">"</span> <span class="s1">'{decision:"block", reason:$reason}'</span>
  <span class="nb">exit </span>0
<span class="k">fi</span>
</code></pre>
</div>
<p>A model that reads the block reason switches itself over to <code>Agent(subagent_type: "general-purpose", model: "opus")</code>. No human has to stop it and retype the command.</p>
<h2>
<p>  What broke was the budget, then the judgment<br />
</p></h2>
<p>The same morning, in another conversation, I was talking about over-engineering. Just before that I had simplified my weekly-report machinery heavily, cutting thousands of lines of code. I typed this:</p>
<blockquote>
<p>A fair amount of the over-engineering happens on opus, after fable&#8217;s usage limit is past</p>
<p>The real problem is that this happens because Fable gets wasted on implementation and review, the usage limit runs past, and I&#8217;m forced to make Opus the orchestrator.</p>
</blockquote>
<p>Making the top model your default breaks the budget first. But that budget was there to buy the judge tier&#8217;s model. When it runs out, the judge tier drops to Opus, and the dropped judge tier waves over-engineering through. Putting the top model in the &#8220;default&#8221; slot works to push it out of the &#8220;judge tier&#8221; slot. Up to here this is an impression from a handful of cases; I have not counted how many times Opus&#8217;s judgment let over-engineering through.</p>
<p>So I reversed the direction in which I plug the leaks. The session default stays at the top model; the paths from the default to the judge tier stay open, and the paths that leak from the default to anything else get cut. The judge tier gets pinned. The <code>architect</code> agent, which decides whether a thing should be built at all, has been the only <code>model: fable</code> in my environment from that day through September 16.</p>
<h2>
<p>  It still ran out every weekend<br />
</p></h2>
<p>Three days later, on Friday, I typed this:</p>
<blockquote>
<p>Fable&#8217;s usage limit always runs out on Saturdays, Sundays and holidays, which is when I use it most. After it&#8217;s gone Opus does the design, and I really do feel Fable&#8217;s absence. I want it focused on design and planning, where Fable is strong, and everything else delegated to other models. Even now it&#8217;s supposed to delegate to Opus when it can, but a lot of the time Fable just goes ahead and implements.</p>
</blockquote>
<p>I had plugged the review path, but implementation itself was running on Fable. My first convention had an escape hatch — if the conditions are not met, this session may implement — and the model itself was making that call.</p>
<p>I changed three things the same day.</p>
<ol>
<li>
<strong>Inverting the default.</strong> The default for &#8220;does this session implement&#8221; became &#8220;hand it to Opus.&#8221; I may implement in place only when I write one line in the plan naming one of three things: prose edits to design documents (decision records such as ADRs), a concrete reason the work cannot be handed off, or an explicit instruction from the user. Accountability moved from the side that hands off to the side that does not</li>
<li>
<strong>A hook right after plan approval.</strong> A PostToolUse on <code>ExitPlanMode</code> reminds Fable sessions, and only Fable sessions, to decide the executor. It fires before the first implementation Edit, the latest safe position available</li>
<li>
<strong>One line in the resident rules.</strong> So that it reaches implementation that never passes through plan mode, I wrote into my rules: &#8220;implementation in a judge-tier session defaults to dispatch to the build tier.&#8221; One line saying a judge-tier session does not implement; it hands the work to a build-tier session</li>
</ol>
<p>The hook in (2) has a condition for staying quiet: if the plan body already contains the set phrase &#8220;executor decision,&#8221; it says nothing. At first I also had general words like <code>dispatch</code> and <code>spawn-session</code> in the suppression list. Running that against 271 past <code>ExitPlanMode</code> calls suppressed 31, and 30 of the 31 were false suppressions. A plan that merely mentions handing implementation off — that is, a session doing nothing but routing tasks, exactly where I most want the hook to fire — would go quiet. I narrowed the suppression list to the single set phrase and put &#8220;a plan that only mentions dispatch still fires&#8221; into the regression tests. Putting only mechanically decidable conditions into the machinery is the same call I made with the review hook.</p>
<p>In the three weeks since, there is no report in my session logs of the usage limit running out. That is the absence of a report, not a record of measured headroom. I still have nothing in my environment that mechanically records how much of the usage limit is left. I also have not looked at whether reviews that ran on Fable were catching anything Opus misses. What I can say goes as far as this: I have not had to write that same report again.</p>
<h2>
<p>  Four decision rules<br />
</p></h2>
<p>If you run a similar setup, these four are what you can take away.</p>
<p><strong>Don&#8217;t leave the tiers to the default; pin the judge tier and plug the paths that leak.</strong> My session default is still <code>fable</code>. What I changed is not the default but the paths the default flows into. It leaks into built-in skills, built-in subagents, and the session&#8217;s own implementation, and a <code>model:</code> in my own agent definitions reaches none of those three.</p>
<p><strong>Fill in the built-in subagent defaults with the environment variable and same-named definitions.</strong> Setting <code>CLAUDE_CODE_SUBAGENT_MODEL=opus</code> changes the default for general-purpose. Explore and Plan do not move on that variable alone: either fix everything to one model with <code>CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1</code>, or place a same-named agent definition carrying a <code>model:</code>. While writing this article I set the environment variable to <code>opus</code> and gave Explore a same-named definition on <code>sonnet</code>. Of the 362 launches above, what remains is Plan&#8217;s 10.</p>
<p><strong>Block when the test is mechanical, advise when it is a heuristic.</strong> Skill name crossed with session model produces no false positives, so it gets stopped before execution. Anything you can only test by partial prompt matching stays a warning. Advice that arrives after execution does not protect a budget.</p>
<p><strong>Invert the default of a convention.</strong> Most &#8220;conventions nobody follows&#8221; put no accountability on the side that does not follow them. Put the default on the handing-off side, and you have to write down your reason for not handing off.</p>
<p>I wrote expiry conditions for this wiring. It comes out if model tier distinctions and usage limits disappear, or if Claude Code starts switching models per session on its own. <code>opusplan</code> (plan on opus, execution on sonnet) already exists, so once a version that drops from fable to opus arrives, the hook is unnecessary.</p>
<h2>
<p>  Sources<br />
</p></h2>
<ul>
<li>
<a href="https://code.claude.com/docs/en/sub-agents" rel="noopener noreferrer">Create custom subagents &#8211; Claude Code Docs</a> — the model resolution order, Explore&#8217;s cap, <code>CLAUDE_CODE_SUBAGENT_MODEL_FORCE</code> (retrieved 2026-09-16)</li>
<li>
<a href="https://code.claude.com/docs/en/model-config" rel="noopener noreferrer">Model configuration &#8211; Claude Code Docs</a> — <code>opusplan</code>, <code>CLAUDE_CODE_SUBAGENT_MODEL</code> (retrieved 2026-09-16)</li>
<li>
<a href="https://github.com/anthropics/claude-code/issues/76514" rel="noopener noreferrer">anthropics/claude-code #76514</a> — subagents in a Fable session all inherit Fable (2026-07-10, closed as not planned)</li>
<li>
<a href="https://github.com/anthropics/claude-code/issues/93894" rel="noopener noreferrer">anthropics/claude-code #93894</a> — one <code>/code-review high</code> on Fable 5.1 exhausts the session budget (2026-09-12, open)</li>
</ul>
<h2>
<p>  Related links<br />
</p></h2>
<ul>
<li>
<a href="https://dev.to/shimo4228/i-handed-41-tasks-to-an-ai-loop-the-bottleneck-was-judgment-not-code-23dp">I Handed 41 Tasks to an AI Loop. The Bottleneck Was Judgment, Not Code</a> — how the split of judgment to Fable and implementation to Opus came about</li>
<li>
<a href="https://dev.to/shimo4228/i-tried-an-opus-orchestrator-and-killed-it-the-roi-of-multi-agent-systems-1f7g">I Tried an Opus Orchestrator and Killed It: The ROI of Multi-Agent Systems</a> — the February record of falling through the same inheritance default, in the downward direction</li>
<li>
<a href="https://dev.to/shimo4228/i-cut-my-ai-review-chain-from-6-stages-to-1-breaking-the-loop-that-never-hits-zero-findings-1moi">I Cut My AI Review Chain From 6 Stages to 1: Breaking the Loop That Never Hits Zero Findings</a> — cutting the number of review stages, on the same morning I noticed the advisory hook&#8217;s double payment</li>
<li>
<a href="https://github.com/shimo4228/zenn-content/blob/main/articles-en/top-model-as-default-leaks.md" rel="noopener noreferrer">The Markdown source of this article (GitHub)</a> — the Markdown for every article, plus the index (docs/PUBLICATIONS.md), lives in the same repository</li>
<li>
<a href="https://github.com/shimo4228" rel="noopener noreferrer">My GitHub</a> — my research repositories, with DOIs</li>
<li>
<a href="https://github.com/shimo4228/claude-harness" rel="noopener noreferrer">claude-harness</a> — the public mirror of this article&#8217;s hook, <code>hooks/review-model-notice.sh</code>, and its tests</li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/i-made-the-top-model-my-session-default-and-one-heavy-implementation-plus-its-review-burned-through-fables-usage-limit.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>92% of dev.to posts get zero reactions, and the title advice does not change that</title>
		<link>https://codango.com/92-of-dev-to-posts-get-zero-reactions-and-the-title-advice-does-not-change-that.htm</link>
					<comments>https://codango.com/92-of-dev-to-posts-get-zero-reactions-and-the-title-advice-does-not-change-that.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 12:12:00 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/92-of-dev-to-posts-get-zero-reactions-and-the-title-advice-does-not-change-that.htm</guid>

					<description><![CDATA[I set out to test the advice. Every few weeks someone publishes a piece explaining what works on dev.to, and the recommendations are always the same: put a number in <a class="more-link" href="https://codango.com/92-of-dev-to-posts-get-zero-reactions-and-the-title-advice-does-not-change-that.htm">Continue reading <span class="screen-reader-text">  92% of dev.to posts get zero reactions, and the title advice does not change that</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>I set out to test the advice. Every few weeks someone publishes a piece explaining what works on dev.to, and the recommendations are always the same: put a number in the title, use all four tags, post on a Tuesday morning, add a cover image. I wanted to check those claims against the platform&#8217;s own data, because dev.to has a public API and nobody seems to have pointed it at this question.</p>
<p>I got an answer, but not to the question I asked. Before you can ask what makes a post do well, you have to know what a normal post looks like, and it turns out almost nobody knows, including me, because the version of dev.to you read is not the one you publish into.</p>
<h2>
<p>  Two websites wearing the same logo<br />
</p></h2>
<p>The feed at dev.to is curated. I pulled 2,500 posts from it and looked at the ones published in August, so everything had at least a fortnight to collect reactions. In that sample of 170 posts, the median post had 15 reactions, 70.6% of them had ten or more, and the mean was 28.8. Exactly zero of them had no reactions at all.</p>
<p>Then I sampled the site itself rather than the feed, by picking article IDs at random and fetching each one directly. That gives 460 posts published in August, chosen without any regard for whether anyone liked them. Here is that sample:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th></th>
<th>curated feed</th>
<th>the actual site</th>
</tr>
</thead>
<tbody>
<tr>
<td>posts sampled (August)</td>
<td>170</td>
<td>460</td>
</tr>
<tr>
<td>mean reactions</td>
<td>28.8</td>
<td>0.21</td>
</tr>
<tr>
<td>zero reactions</td>
<td>0.0%</td>
<td>91.7%</td>
</tr>
<tr>
<td>ten or more reactions</td>
<td>70.6%</td>
<td>0.4%</td>
</tr>
<tr>
<td>zero comments</td>
<td>—</td>
<td>95.7%</td>
</tr>
</tbody>
</table>
</div>
<p>Ninety-two per cent of posts, plus or minus about two and a half points at this sample size, get no reaction whatsoever. Not a low number of reactions. None. And 95.7% of them get no comments either.</p>
<p>These are not bad posts. The zero-reaction pile, read at random, looks like this:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>0 reactions  Four JavaScript problems I hit writing a chess engine
0 reactions  Delta E is a distance, not a score
0 reactions  How Farm.js compiles React components into direct DOM updates
0 reactions  Why Zod Crashed My Node.js Server
</code></pre>
</div>
<p>I would read all four of those. They went out into total silence, and the feed I actually see has been quietly filtering them out of my view for as long as I have been posting here.</p>
<h2>
<p>  Getting a fair sample is the hard part<br />
</p></h2>
<p>My first attempt was the obvious one. dev.to&#8217;s API lets you list articles by tag, so I paged through 39 popular tags and collected 82,749 posts. Then, before trusting any of it, I ran a check: the curated feed&#8217;s August posts are popular by construction, so my big corpus ought to contain nearly all of them. It contained 49%.</p>
<p>So I took one missing post, a <code>webdev</code> article with 60 reactions published on 31 August, and walked twelve pages of the <code>webdev</code> tag listing looking for it. It is not there. What is there is stranger:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>page 10: newest=2026-08-31  oldest=2026-08-29
page 11: newest=2026-09-13  oldest=2026-08-27
page 12: newest=2026-08-27  oldest=2026-08-18
</code></pre>
</div>
<p>Page 11 contains posts newer than anything on page 10. The pages overlap, they run backwards and forwards through time, and posts go missing between them. The listing is not an ordered, complete view of a tag, so you cannot page it to build a corpus, which is precisely what I had just spent forty minutes doing.</p>
<p>Worse, the omissions are not random. Comparing my random-ID sample against the tag corpus, the posts the listing gave me averaged 0.40 reactions while the ones it skipped averaged 0.15, and 19.0% of the included posts had at least one reaction against 5.1% of the excluded. Sampling through the listing quietly hands you a rosier site than the real one. My first draft of this post, written off that corpus, said 85% of posts get nothing. The true figure is 92%, and the error was entirely my instrument.</p>
<p>I also checked that a fortnight really is long enough for a post to finish accumulating. Splitting the August posts by age at the time I fetched them, the share getting any reaction was 13.6% at 14-20 days old and 15.0% at 42-48 days, with no trend in between, so nothing meaningful arrives after the first couple of weeks.</p>
<h2>
<p>  What actually correlates with anything<br />
</p></h2>
<p>For the finer comparisons I used the large tag corpus, 45,400 August posts, because the random sample is too small to slice. It leans optimistic in absolute terms, for the reason above, but the relative differences between groups are what matter here.</p>
<p>The advice is not all wrong. A cover image goes with 23.7% of posts getting at least one reaction against 11.2% without. Writing in the first person, a title with &#8220;I&#8221; or &#8220;my&#8221; in it, goes with 24.7% against 13.9%. Length helps up to a point, with posts of three to seventeen minutes&#8217; reading time doing better than posts under three minutes. Four tags beat one, by 16.0% against 3.5%.</p>
<p>Two pieces of standard advice are simply backwards. Titles beginning with &#8220;How&#8221; did worse than titles that do not, 12.2% against 15.1%. Listicles, the numbered-list titles, got at least one reaction less often than other posts, 9.5% against 15.0%, though their mean was higher, 0.80 against 0.50, which is the signature of a format that mostly flops and occasionally spikes.</p>
<p>The tag you choose matters more than anything you do to the title. Among tags with at least 300 posts:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>tag</th>
<th>posts</th>
<th>any reaction</th>
</tr>
</thead>
<tbody>
<tr>
<td>css</td>
<td>560</td>
<td>38.8%</td>
</tr>
<tr>
<td>showdev</td>
<td>960</td>
<td>38.2%</td>
</tr>
<tr>
<td>frontend</td>
<td>541</td>
<td>33.6%</td>
</tr>
<tr>
<td>typescript</td>
<td>1,556</td>
<td>30.1%</td>
</tr>
<tr>
<td>crypto</td>
<td>926</td>
<td>1.1%</td>
</tr>
<tr>
<td>jobs</td>
<td>463</td>
<td>0.9%</td>
</tr>
<tr>
<td>defi</td>
<td>925</td>
<td>0.1%</td>
</tr>
<tr>
<td>tech</td>
<td>800</td>
<td>0.0%</td>
</tr>
</tbody>
</table>
</div>
<p>Eight hundred posts tagged <code>tech</code> in one month, and not one of them got a single reaction.</p>
<p>And posting more is associated with doing worse per post, not better. Authors publishing one to five posts in August had 23.3% of them get a reaction; authors publishing 21 to 25 had 7.5%. Those high-volume authors are not a curiosity either, they produced 18,386 of the 45,400 posts in the corpus.</p>
<h2>
<p>  The part that undoes most of the above<br />
</p></h2>
<p>All of those comparisons share a flaw. If experienced writers with existing followers are also the people who add cover images and write in the first person, then I am measuring the audience, not the technique.</p>
<p>So I ran it again inside each author. For every author with at least four posts in the month, I compared their own posts with a given feature against their own posts without it, then looked at the distribution of those paired differences. If a first-person title genuinely helps, an author&#8217;s first-person posts should beat their other posts.</p>
<p>They do not. Across 254 authors, the median paired difference for first-person titles was −0.08 reactions, with a 95% confidence interval of [−0.13, +0.00]. Listicles came out at −0.08, [−0.12, −0.03]. Question-mark titles, −0.08, [−0.14, −0.04]. Colons, 0.00. Every apparent effect from the previous section collapses to nothing, or to very slightly negative, once each writer is compared against themselves.</p>
<p>The honest reading is that the title patterns were never doing the work. They were a marker of which kind of account was posting, and when you hold the account fixed they stop predicting anything. What survives the within-author test is not the phrasing, it is which tag you land in and whether anyone was already reading you.</p>
<p>There was one result I could not explain away, and the sample is small enough that I would not lean on it: the 273 posts carrying dev.to&#8217;s new AI-assisted disclosure label averaged 4.06 reactions with 11.0% reaching ten, against 0.48 and 0.7% for everything else. That is either a real effect or, more likely, evidence that the people who bother setting a brand-new metadata field are unusually engaged with the platform to begin with.</p>
<h2>
<p>  Where I sit, since it would be cowardly not to say<br />
</p></h2>
<p>My last twenty posts run from 5 to 16 reactions, median 8. Against the random sample, a post with 8 reactions beats 99.6% of dev.to. Sixteen beats everything in a 460-post draw.</p>
<p>That sounded like good news for about ten seconds. What it actually means is that the bar is on the floor: writing something that a handful of people react to puts you in the top half of one per cent, and the median experience of publishing here is being read by nobody at all.</p>
<h2>
<p>  What I got wrong<br />
</p></h2>
<p>Twice, in the same afternoon, and both were instrumentation rather than analysis.</p>
<p>The first fetch piped its progress through <code>tail</code>, which buffers, so I watched an empty log for twenty minutes and assumed it had hung. It had not; it had collected 20,596 posts and was holding them in memory to write at the end. I killed it and lost all of them.</p>
<p>The second is the one that would have ruined the post. I had a complete-looking corpus of 82,749 posts and a headline number, and the only reason I did not publish 85% is that I made myself check the corpus against a list of posts I knew should be in it. If I had skipped that check, everything above would read the same and be wrong by seven percentage points, in the flattering direction. The check took four minutes.</p>
<h2>
<p>  What to take from it<br />
</p></h2>
<p>If you write here, the relevant comparison is not the feed. The feed is the top fraction of a per cent, and measuring yourself against it is like judging your running against the people on television.</p>
<p>Pick your tag deliberately, because that is the one input with a large and consistent association. Put a cover image on it. Then stop optimising the title, because within a given author it does not appear to matter, and spend the time on the thing that does: being worth following, so that the next post starts with an audience rather than hoping to find one.</p>
<p>And if you are going to publish numbers about a platform, check your sample against something you already know the answer to. Mine was wrong in the direction that made the story nicer, which is the direction these things usually fail in.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/92-of-dev-to-posts-get-zero-reactions-and-the-title-advice-does-not-change-that.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Guardrails for AI-Assisted Development: Skills, Gates, Hooks and Mutation Tests</title>
		<link>https://codango.com/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests.htm</link>
					<comments>https://codango.com/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 12:09:10 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests.htm</guid>

					<description><![CDATA[When a model writes the code, writing it stops being the expensive part. Proving you didn&#8217;t break something nobody was looking at becomes the expensive part. That sentence took me <a class="more-link" href="https://codango.com/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests.htm">Continue reading <span class="screen-reader-text">  Guardrails for AI-Assisted Development: Skills, Gates, Hooks and Mutation Tests</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>When a model writes the code, writing it stops being the expensive part. Proving you didn&#8217;t break something nobody was looking at becomes the expensive part.</p>
<p>That sentence took me a few months of building to arrive at, and once I had it, most of my tooling decisions stopped being arguments.</p>
<p>This is a walkthrough of the verification layer in a real project of mine: a wellbeing analytics platform with a Next.js core and a Python AI service. Four agent skills, sixteen repository gates, a hook that runs them at edit time, and a narrow mutation testing pilot. Everything here is running code, and I&#8217;ll be specific about what each piece does not catch, because that turns out to be the more useful half.</p>
<h2>
<p>  Why conventions in a README don&#8217;t survive<br />
</p></h2>
<p>Let me start with the failure that convinced me.</p>
<p>My repo has an <code>AGENTS.md</code> saying that <code>.agents/skills/</code> is the single source of agent instructions. A month earlier, <code>GEMINI.md</code> had said in plain prose: don&#8217;t create client-local copies of these skills.</p>
<p>A 3,442-byte partial copy of one skill showed up in <code>.gemini/skills/</code> anyway. Two weeks later there was still an empty leftover directory next to it.</p>
<p>Here&#8217;s the part that matters: <code>.claude/</code> and <code>.gemini/</code> are in <code>.gitignore</code>. The copy was invisible in every diff and every code review. Not missed by a reviewer. Invisible to the mechanism of reviewing.</p>
<p>Two copies both claiming to be canonical, drifting apart with nothing comparing them. Prose has no enforcement mechanism. That&#8217;s not a discipline problem, it&#8217;s a category problem.</p>
<h2>
<p>  Layer 1: a skill tree with exactly one root<br />
</p></h2>
<p>Skills are markdown files that tell an agent how to work in this repository; mine cover product invariants, session/branch state, verification, and the gates themselves. The design has four rules.</p>
<p><strong>One canonical location.</strong> <code>.agents/skills/</code> and nowhere else. There is no list of skills in any config file. The canonical set is the directories on disk:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">skillNames</span> <span class="o">=</span> <span class="nx">fs</span>
  <span class="p">.</span><span class="nf">readdirSync</span><span class="p">(</span><span class="nx">SKILLS_ROOT</span><span class="p">,</span> <span class="p">{</span> <span class="na">withFileTypes</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
  <span class="p">.</span><span class="nf">filter</span><span class="p">((</span><span class="nx">entry</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">entry</span><span class="p">.</span><span class="nf">isDirectory</span><span class="p">())</span>
  <span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">entry</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
</code></pre>
</div>
<p>A folder appears, a skill exists. This is why a copy elsewhere is dangerous rather than merely redundant: if the filesystem is the source of truth, any folder with a familiar name becomes a second source of truth.</p>
<p><strong>Routing in the entrypoint.</strong> <code>AGENTS.md</code> carries a plain markdown list mapping tasks to skills:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight markdown"><code><span class="p">-</span> Verifying, testing, proving a fix, checking readiness, reviewing evidence, or
  about to claim a substantive change is complete →
  <span class="sb">`.agents/skills/shalomut-verification/SKILL.md`</span>.
</code></pre>
</div>
<p>Why bother if some clients discover skills automatically? Because a client with discovery and a human reading the file directly must arrive at the same rules. Discovery changes how a file is found, never what it says.</p>
<p><strong>A reading map inside each skill.</strong> Every skill has a <em>How to read this skill</em> section splitting its own sections into &#8220;always in force&#8221; and &#8220;open when this condition holds.&#8221; That map, not the entrypoint, decides how much of a skill a task needs. The rule next to it: loading a section is cheap, skipping a rule is not, so when a condition is ambiguous, open the section.</p>
<p><strong>A check that all of the above holds.</strong> <code>npm run lint:skills</code> refuses: a skill copy outside the canonical root (including an empty directory named after one), a broken or orphaned link in <code>references/</code>, a <code>##</code> section the reading map never classifies, and any client entrypoint that routes nowhere.</p>
<h2>
<p>  Layer 2: gates, or how a convention becomes unignorable<br />
</p></h2>
<p>A gate is a script that reads the repository and exits non-zero when it finds a violation. Nothing executes. No arguments get fuzzed, no mutants get generated. It reads — not just paths, but anything knowable without running the program: where a repository gets constructed (<code>lint:composition</code>), what a response body contains (<code>lint:error-bodies</code>), whether a document still agrees with the configuration it quotes (<code>lint:doc-numbers</code>).</p>
<h3>
<p>  What actually deserves a gate<br />
</p></h3>
<p>This is the decision rule I&#8217;d most want to hand to someone starting out, and it took me a while to get right:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Kind of rule</th>
<th>Where it belongs</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lives inside a module, expressed through its API</td>
<td>An ordinary test. No gate.</td>
</tr>
<tr>
<td>About the shape of the repository: what may be imported, where a literal may stand, which interpreter runs, where skills live</td>
<td>A gate</td>
</tr>
<tr>
<td>Editable source with a derived copy</td>
<td>A <code>--check</code> mode on the generator, not a second equality test</td>
</tr>
<tr>
<td>A machine cannot judge it: is this architecture right, is this audit record genuinely closed</td>
<td>Prose. And say so in the gate&#8217;s doc comment, so a green gate never reads as proof of what it never checked</td>
</tr>
</tbody>
</table>
</div>
<p>The tell that you need a gate: the violation is silent. Tests green, build passing, reviewer sees nothing, and the rule is already broken. If a violation fails the suite anyway, a test is enough and a gate is overhead.</p>
<h3>
<p>  One gate in full<br />
</p></h3>
<p>The audit of my repo counted 21 route handlers interpolating a raw <code>error.message</code> into what they sent back. On <code>/api/auth/login</code> that went to anyone. Every one was written by somebody being helpful.</p>
<p>The naive check is a regex for <code>error.message</code>. I wrote that first. It let <code>(error as Error).message</code> straight through, and the audit itself had missed <code>error?.message</code> for the same reason.</p>
<p>So the rule became two rules:</p>
<ol>
<li>a <code>catch</code> in a route handler binds the name <code>error</code>;</li>
<li>the argument of <code>NextResponse.json(...)</code> never mentions it.</li>
</ol>
<p>Rule 1 exists to make rule 2 complete. Refusing the whole identifier makes the spelling irrelevant, and an identifier rule is only as good as the identifier, so rule 1 stops a handler slipping past with <code>catch (e)</code>.</p>
<p>The implementation is not a regex either. It&#8217;s three passes:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="c1">// 1. Strip strings and comments, but KEEP ${...} inside templates.</span>
<span class="c1">//    Without stripping, { error: 'Internal error' } fails on its own wording.</span>
<span class="c1">//    Stripping templates whole would let `failed: ${error}` through,</span>
<span class="c1">//    and that is the leak itself.</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nf">stripTextAndComments</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* hand-written scanner */</span> <span class="p">}</span>

<span class="c1">// 2. Find the argument region of a literal call by counting parens.</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nf">argumentRegions</span><span class="p">(</span><span class="nx">source</span><span class="p">,</span> <span class="nx">call</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">NextResponse.json(</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="c1">// 3. Only now, a pattern. A property access is fine (`produced.error` is our</span>
<span class="c1">//    own field). A spread is not: { ...error } puts the whole thing in the body.</span>
<span class="k">new</span> <span class="nc">RegExp</span><span class="p">(</span><span class="s2">`(?&lt;![</span><span class="se">\</span><span class="s2">w$])(?&lt;!(?&lt;!</span><span class="se">\</span><span class="s2">.)</span><span class="se">\</span><span class="s2">.)</span><span class="p">${</span><span class="nx">BINDING</span><span class="p">}</span><span class="se">\</span><span class="s2">b`</span><span class="p">).</span><span class="nf">test</span><span class="p">(</span><span class="nx">readable</span><span class="p">)</span>
</code></pre>
</div>
<p>Two places in the codebase used the name <code>error</code> for the product&#8217;s own refusal wording. They got renamed rather than exempted. An exemption list is a place the next leak hides.</p>
<p>And the blind spot, stated in the doc comment rather than discovered later: a body assembled into a variable and passed by name goes unnoticed. That gap is accepted, because the alternative is parsing TypeScript in a fitness check and every occurrence the audit found was a literal at the call site. With the follow-up that matters: if that stops being true, this needs a parser, not a wider regular expression.</p>
<h3>
<p>  The shape every gate shares<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="c1">// scripts/check-&lt;subject&gt;.mjs</span>
<span class="cm">/**
 * &lt;The rule. Why it exists — name the actual incident. What this cannot see.&gt;
 */</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nf">findViolations</span><span class="p">(</span><span class="nx">source</span><span class="p">,</span> <span class="nx">file</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* pure, returns string[] */</span> <span class="p">}</span>

<span class="kd">function</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* reads files, prints, process.exit(1) */</span> <span class="p">}</span>
<span class="k">if </span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">===</span> <span class="nf">fileURLToPath</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="nf">main</span><span class="p">();</span>
</code></pre>
</div>
<p>Pure exported functions so the test can import them. <code>main()</code> guarded so importing doesn&#8217;t run it. And on success, a line saying how much was checked:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>Error-body fitness check passed: 47 route handlers, no caught error in a response body.
</code></pre>
</div>
<p>A silent success is indistinguishable from a check that read nothing.</p>
<h3>
<p>  Wiring, and why every gate has its own test<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="nl">"lint:error-bodies"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node --test scripts/check-error-bodies.test.mjs &amp;&amp; node scripts/check-error-bodies.mjs"</span><span class="err">,</span><span class="w">
</span><span class="nl">"verify:core"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run lint:literals &amp;&amp; ... &amp;&amp; npm run typecheck &amp;&amp; npm test &amp;&amp; npm run lint &amp;&amp; npm run build"</span><span class="w">
</span></code></pre>
</div>
<p>The gate&#8217;s test runs before the gate. That order is the whole point: prove the check can fail, then trust it. A check whose pattern never matches anything is worse than no check, because it manufactures confidence.</p>
<h3>
<p>  Gates that guard the gates<br />
</p></h3>
<p>The cheapest way to fix a red gate is to delete it from the chain. So:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>lint:gate-inventory refuses:
  - a gate that isn't a step of verify:core
  - a gate missing from the inventory table
  - an inventory row with no gate behind it
  - a lint:* command that doesn't run its own test
</code></pre>
</div>
<p>Without this, the whole safety system rests on good faith, which means it doesn&#8217;t rest on anything.</p>
<p>Same principle inside the skills: the guardrails skill says outright that weakening a check, adding a file to an exemption list or narrowing its scope is a change to the rule, not a fix to the build — do it deliberately, update the doc comment and the tests on both sides. And: never bring a gate&#8217;s test in line with current behaviour to make things green. The gate&#8217;s test is the record of the rule.</p>
<h2>
<p>  Layer 3: the hook, so the agent hears it now<br />
</p></h2>
<p>CI is the record. But CI tells you at 11:00 about something written at 10:00, by which point it&#8217;s buried under later edits made on top of it.</p>
<p><code>.claude/settings.json</code> is the one file under <code>.claude/</code> that Git tracks — <code>.gitignore</code> un-ignores exactly it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"PostToolUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
      </span><span class="nl">"matcher"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Edit|Write|MultiEdit|NotebookEdit"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"command"</span><span class="p">,</span><span class="w"> </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node scripts/gate-hook.mjs"</span><span class="p">,</span><span class="w"> </span><span class="nl">"timeout"</span><span class="p">:</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="p">}]</span><span class="w">
    </span><span class="p">}]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p><code>gate-hook.mjs</code> holds no rules. It takes the path of the file just written, maps it to the gates that could care, and runs those:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">RULES</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span> <span class="na">gate</span><span class="p">:</span> <span class="dl">'</span><span class="s1">lint:error-bodies</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">when</span><span class="p">:</span> <span class="p">(</span><span class="nx">file</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="sr">/^src</span><span class="se">/</span><span class="sr">app</span><span class="se">/</span><span class="sr">api</span><span class="se">/</span><span class="sr">.*</span><span class="se">/</span><span class="sr">route</span><span class="se">.</span><span class="sr">ts$/</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">file</span><span class="p">)</span> <span class="p">},</span>
  <span class="c1">// ...</span>
<span class="p">];</span>
</code></pre>
</div>
<p>On refusal it exits with code 2, which feeds stderr back to the agent, and the message points to the relevant skill section and explicitly says not to weaken the check or rewrite its test to make it pass. That last line is defence against the first thing an agent tries.</p>
<p>Three limits, all stated in the file itself:</p>
<ul>
<li>
<strong>15 of 16 gates are mapped.</strong> <code>lint:literals</code> is deliberately out: its Python half needs an interpreter from <code>.venv</code>, and a missing local environment must not read as a violation.</li>
<li>
<strong>The mapping is narrow on purpose.</strong> A gate that fires on every edit is a gate people learn to wait out.</li>
<li>
<strong>It runs in one client only.</strong> A rule that lives in a hook doesn&#8217;t exist for Copilot, for Gemini, or for a person in a terminal.</li>
</ul>
<p>One implementation detail worth stealing: the hook runs only the check half of each command, not the <code>node --test</code> half. The first version buried a one-line violation under sixty lines of passing TAP.</p>
<h2>
<p>  Layer 4: tests, and proving the tests are worth anything<br />
</p></h2>
<p>Gates check shape. Tests check behaviour. Neither checks whether the tests are any good. That&#8217;s mutation testing: deliberately corrupt the code, see whether tests notice. Mine runs on two files — the AI contract validator and the scoring bands — and the full run isn&#8217;t a CI gate: the score moves when a function migrates between files or a test file enters the list, which has nothing to do with test strength.</p>
<p>The checks around it keep that number meaningful. <code>lint:mutation-config</code> re-derives <code>tap.testFiles</code> from the repo, because a missing entry doesn&#8217;t lower the score honestly — it reports as survived a mutant a real test would have killed. <code>lint:contract-refusals</code> demands a negative-test suite for every contract version; it proves a suite exists, not that it is complete. And when a product rule leaves a mutated file, <code>mutate</code> follows it in the same change, or the rule silently drops out of measurement.</p>
<h2>
<p>  Layer 5: how much checking is enough<br />
</p></h2>
<p>This is the part I&#8217;d defend hardest, and the part most teams skip.</p>
<p>My verification skill carries a selection matrix: rows are areas a diff touched, cells are the mandatory minimum. The principle: choose the smallest set of checks that proves the changed behaviour, then widen in proportion to risk.</p>
<p>A few rows:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Changed</th>
<th>Mandatory minimum</th>
</tr>
</thead>
<tbody>
<tr>
<td>Markdown / skills only</td>
<td>Frontmatter and links, <code>git diff --check</code>, <code>lint:skills</code>
</td>
</tr>
<tr>
<td>A repository gate</td>
<td>Its paired test and the gate, <code>lint:gate-inventory</code>. Weakening the check = changing the rule</td>
</tr>
<tr>
<td>
<code>src/app/api</code>, services</td>
<td>Nearest tests, then <code>npm test</code> and <code>npm run build</code>
</td>
</tr>
<tr>
<td>AI contract</td>
<td>
<code>lint:contract-refusals</code>, contract/registry/client tests, Python tests, local boundary E2E</td>
</tr>
<tr>
<td>Python dependencies</td>
<td>
<code>lint:python-deps</code>; production runs 3.11 and dev machines usually don&#8217;t have it, so the one real proof is <code>docker build</code> plus the suite inside that image</td>
</tr>
<tr>
<td>Auth or secrets</td>
<td>Unauthorized / missing-secret / tenant-isolation tests plus a security-focused diff review</td>
</tr>
</tbody>
</table>
</div>
<p>Running everything for a docs edit is the same mistake as running one small test for an auth change. Both look like diligence. Neither is.</p>
<p>One check belongs to no row: <code>typecheck</code> is mandatory for any <code>.ts</code> change. <code>build</code> types only the application graph, <code>lint</code> doesn&#8217;t check types at all, and <code>npm test</code> runs through <code>tsx</code>, which strips types without checking them. A green test run tells you nothing about types.</p>
<h3>
<p>  Say what counts as evidence<br />
</p></h3>
<p>With an agent this stops being pedantry. Models report affirmatively by default, so the rules are written down:</p>
<ul>
<li>Label the context — local, test, or deployed — and never mix them silently.</li>
<li>
<code>npm run dev</code> starts a runtime. It is not evidence.</li>
<li>A mock MCP server is not proof of real transport, and a wrapper that delegates to the same command is not a second piece of evidence.</li>
<li>Don&#8217;t promise repo-wide mutation coverage. Separate killed / survived / no-coverage / runtime-error.</li>
<li>
<code>verify:core</code> is an <code>&amp;&amp;</code> chain: it stops at the first failure. The steps after it did not run — don&#8217;t report them as passed.</li>
</ul>
<h2>
<p>  Every rule carries its incident<br />
</p></h2>
<p>The habit I&#8217;d transplant into any codebase regardless of AI: each check&#8217;s doc comment names what broke, with a date.</p>
<ul>
<li>
<strong>Fonts.</strong> Until 2026-08-12 the build downloaded five <code>.woff2</code> files from a Google host; when a runner got a stale stylesheet, all five 404&#8217;d and the build failed with a message mentioning neither fonts nor the network. The same commit built cleanly in the neighbouring job, so the gate had become a coin toss instead of a red light.</li>
<li>
<strong>Tenant chokepoint.</strong> A context loader recorded the visit from the request rather than the answer, so an administrator reading the only tenant left no audit row at all, and no test failed.</li>
</ul>
<p>The corollary is a cleanup rule: a rule with no incident behind it is a candidate for deletion. Either it isn&#8217;t needed, or nobody remembers what it defends — and the first person it inconveniences will remove it.</p>
<h2>
<p>  What this doesn&#8217;t do<br />
</p></h2>
<ul>
<li>
<strong>Gates read text.</strong> A determined rename or a dynamic call walks around them. They defend against accidental violation, not adversarial.</li>
<li>
<strong>Presence checks don&#8217;t check agreement.</strong> A client adapter passes if it contains the string <code>.agents/skills</code>, even if the surrounding sentence says the opposite. Stated in the code; it stays a review question.</li>
<li><strong>None of this judges whether an architectural decision is right.</strong></li>
<li>
<strong>It costs.</strong> Sixteen gates is sixteen scripts plus sixteen tests plus an inventory plus a gate guarding all of it. On a two-week prototype that&#8217;s absurd. In a regulated domain where the question is &#8220;prove this data couldn&#8217;t leak,&#8221; it&#8217;s the cheapest answer I know.</li>
</ul>
<h2>
<p>  If you&#8217;re starting tomorrow<br />
</p></h2>
<ol>
<li>Take the last silent bug you shipped. Write the smallest script that would have refused it. One rule.</li>
<li>Give it a test that proves it can fail, and wire the test to run first.</li>
<li>Put it in one verify chain with everything else, and put that chain in CI.</li>
<li>Write the incident in the doc comment, with the date, and write down what the check can&#8217;t see.</li>
<li>Only then hook it into your agent&#8217;s edit loop, so the refusal arrives while the context is still warm.</li>
</ol>
<p>Steps 1 through 4 work with no AI involved at all. Step 5 is just the feedback arriving earlier. That ordering is deliberate: the reason any of this holds is that the rules live where every client and every human reads them, and the agent integration is the last mile rather than the foundation.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-Based Recovery Didn&#8217;t Eliminate USB Drives. It Eliminated Recovery Independence.</title>
		<link>https://codango.com/cloud-based-recovery-didnt-eliminate-usb-drives-it-eliminated-recovery-independence.htm</link>
					<comments>https://codango.com/cloud-based-recovery-didnt-eliminate-usb-drives-it-eliminated-recovery-independence.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 12:07:38 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/cloud-based-recovery-didnt-eliminate-usb-drives-it-eliminated-recovery-independence.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fh3den1bjmf9dkckzm1xk-gNdiZH-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />Cloud-based recovery is becoming the default way Windows 11 recovers a machine that won&#8217;t boot, and almost nobody signed off on the dependency that creates. Local recovery media was never <a class="more-link" href="https://codango.com/cloud-based-recovery-didnt-eliminate-usb-drives-it-eliminated-recovery-independence.htm">Continue reading <span class="screen-reader-text">  Cloud-Based Recovery Didn&#8217;t Eliminate USB Drives. It Eliminated Recovery Independence.</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fh3den1bjmf9dkckzm1xk-gNdiZH-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>Cloud-based recovery is becoming the default way Windows 11 recovers a machine that won&#8217;t boot, and almost nobody signed off on the dependency that creates. Local recovery media was never the preferred repair path. It was the path that worked when every preferred path had already failed — no network, no identity provider, no vendor cloud, nothing that depended on the thing that just broke. That property is what&#8217;s disappearing, not the USB drive itself.</p>
<p><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%2Fh3den1bjmf9dkckzm1xk.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fh3den1bjmf9dkckzm1xk.jpg" alt="cloud-based recovery — authority chain moving from organization-controlled media to vendor-controlled infrastructure" width="800" height="437" /></a></p>
<h2>
<p>  What Changed<br />
</p></h2>
<p>Microsoft&#8217;s Cloud Rebuild feature moved from the Experimental Insider channel to Beta on September 9, 2026 (Build 26220.9343) — the clearest signal yet that this is headed toward general availability on a normal timeline, not sitting as a permanent preview curiosity. It runs from the Windows Recovery Environment (WinRE), reformats the system disk, and pulls a fresh Windows 11 image plus current drivers directly from Windows Update. No USB flash drive. No DVD. No dependency on a recovery partition surviving whatever failure triggered the rebuild in the first place.</p>
<p>For managed fleets, Cloud Rebuild integrates with Windows Autopilot and Intune: a rebuilt device can re-enroll automatically, with assigned apps, policies, and user settings restored through Backup for Organizations. Remote initiation isn&#8217;t available in this Beta — a rebuild still has to be triggered locally from WinRE or an elevated prompt — but Microsoft has said Intune-driven remote triggering is planned. That detail matters more than it looks. The roadmap points toward IT no longer needing physical hands on the device to initiate recovery at all, which only deepens the dependency this post is about.</p>
<h2>
<p>  Cloud-Based Recovery Improves Convenience. It Doesn&#8217;t Improve Recovery Architecture.<br />
</p></h2>
<p>Microsoft&#8217;s case for this feature is not weak, and it&#8217;s worth stating plainly before making the counter-argument:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Cloud-Based Recovery Improves</th>
<th>Cloud-Based Recovery Weakens</th>
</tr>
</thead>
<tbody>
<tr>
<td>Media management — no drives to provision, label, store, or lose</td>
<td>Recovery independence — recovery no longer executes without external services</td>
</tr>
<tr>
<td>Image freshness — always the current build, not whatever was burned to media months ago</td>
<td>Recovery sovereignty — the organization no longer controls what recovery depends on</td>
</tr>
<tr>
<td>Driver currency — pulled live from Windows Update, not a stale recovery partition</td>
<td>Offline recoverability — no path left that survives a connectivity or vendor outage</td>
</tr>
<tr>
<td>Support simplicity — one documented process instead of per-device media variance</td>
<td>Vendor dependency isolation — the recovery path and the outage&#8217;s blast radius can now overlap</td>
</tr>
</tbody>
</table>
</div>
<p>Every item on the left is real and worth having. None of them is the same claim as the items on the right, and collapsing the two into a single &#8220;this is better&#8221; verdict is the mistake worth avoiding here. Cloud-based recovery can be operationally superior to what it replaces while simultaneously making the recovery architecture more centralized and more dependent on a single vendor&#8217;s availability. Those are two different arguments. Only one of them showed up in the feature announcement.</p>
<h2>
<p>  The Last Independent Recovery Path<br />
</p></h2>
<p>Local recovery media was rarely anyone&#8217;s first choice. It was slow, it required physical custody of a drive, and it depended on someone remembering where that drive was kept. None of that was the point. The point was that it sat entirely outside the dependency chain that caused the outage in the first place — no network path, no identity provider, no vendor&#8217;s uptime, nothing the original failure could have taken down along with everything else. It&#8217;s the same principle <a href="https://www.rack2cloud.com/connected-air-gap-backup-isolation/" rel="noopener noreferrer">the connected air gap</a> argument makes about backup isolation — a fallback only counts as independent if it&#8217;s actually outside the failure domain, not merely labeled that way.</p>
<p><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%2Fgbhh7de9w0q1l4mbhrra.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fgbhh7de9w0q1l4mbhrra.jpg" alt="Traditional recovery path versus cloud-based recovery path — dependency count comparison" width="800" height="389" /></a></p>
<p>Two recovery paths responding to the identical failure make the difference concrete:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Traditional Recovery Path</th>
<th>Cloud-Based Recovery Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>OS corruption</td>
<td>OS corruption</td>
</tr>
<tr>
<td>→ Boot from USB</td>
<td>→ Requires internet connectivity</td>
</tr>
<tr>
<td>→ Reinstall from local media</td>
<td>→ Requires Windows Update reachability</td>
</tr>
<tr>
<td>→ System recovered — one dependency, and it&#8217;s physical custody</td>
<td>→ Requires Microsoft service availability</td>
</tr>
<tr>
<td></td>
<td>→ System recovered — three new dependencies, and none of them belong to the organization</td>
</tr>
</tbody>
</table>
</div>
<p>The traditional path has one dependency: whoever holds the drive. The cloud-based path has at least three, and all three sit outside the machine being recovered and outside the organization&#8217;s authority to guarantee.</p>
<p><a href="https://rack2cloud.com/downloads/carousels/cloud-based-recovery-independence-carousel-v1.pdf" rel="noopener noreferrer">Cloud-Based Recovery Carousel — 8-slide breakdown</a> — the dependency chain argument in slide form: traditional vs. cloud-based recovery paths, the five-layer dependency stack, and the governance question underneath it.</p>
<h2>
<p>  Recovery Authority Transfer<br />
</p></h2>
<p>Every recovery mechanism has an implicit authority chain — the sequence of things that have to hold true, and hold trusted, before a device comes back into service. For local recovery media, that chain was short: the organization decided what image and drivers a device would run, wrote that decision to media it physically controlled, and recovery executed against artifacts nobody outside the organization ever touched.</p>
<p>Cloud-based recovery replaces that chain with a longer one: Microsoft&#8217;s Update infrastructure, generic internet connectivity, DNS resolution, the TLS trust chain validating the connection, and the reachability of whichever Microsoft endpoint actually serves the rebuild. None of those five things sits under the organization&#8217;s authority, and all five now stand between &#8220;device won&#8217;t boot&#8221; and &#8220;device recovered.&#8221;</p>
<p>That&#8217;s the same drift <a href="https://www.rack2cloud.com/recovery-boundary-dependency-failure/" rel="noopener noreferrer">The System Recovered. Your Recovery Boundary Didn&#8217;t.</a> names on the other side of the lifecycle — dependency boundaries expanding faster than recovery boundaries account for. That post is about what a recovered system still depends on afterward. This is about what the recovery mechanism itself now depends on before it can even start.</p>
<p>That&#8217;s the question worth asking before this ships to production fleets, not after: has recovery authority moved outside the recovery boundary? <a href="https://www.rack2cloud.com/recovery-design-boundary/" rel="noopener noreferrer">Recovery Design Boundary</a> — the framework naming what dependencies must exist before recovery can even begin — is built for exactly this question. A recovery mechanism that requires DNS resolution, TLS validation, and a third party&#8217;s infrastructure to stay healthy before it can execute has quietly redrawn where that boundary sits, whether or not anyone updated the architecture diagram to reflect it.</p>
<p>The question is not whether these dependencies usually work. They usually do. The question is whether they remain available during the exact class of event recovery procedures exist to address. Recovery mechanisms are not evaluated during normal operations. They&#8217;re evaluated during abnormal conditions, when multiple assumptions fail simultaneously.</p>
<p><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%2Fm2wpfk8cs3xb9yctdazs.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fm2wpfk8cs3xb9yctdazs.jpg" alt="Recovery dependency surface — DNS, TLS, and vendor infrastructure standing between failure and recovery" width="800" height="437" /></a></p>
<h2>
<p>  Who Approved This?<br />
</p></h2>
<p>Somewhere in most <a href="https://www.rack2cloud.com/data-protection-architecture-strategy-guide/" rel="noopener noreferrer">Data Protection</a> programs there&#8217;s a decision record for which backup platform runs, what the RPO/RTO targets are, and who holds authority to declare a recovery complete. There is almost never a decision record for &#8220;OS recovery may now depend on Microsoft&#8217;s cloud infrastructure being reachable.&#8221; Nobody voted on that. It arrives as a default in an OS update, and it will reach most fleets the same way every other default setting does — silently, on whatever patch cadence the organization already runs.</p>
<p>Organizations rarely lose recovery independence through a formal architecture review. They lose it through default-path drift — a sequence of vendor defaults that gradually become production architecture without ever appearing in a design review.</p>
<p>The <a href="https://www.rack2cloud.com/data-protection-resiliency-learning-path/governance-recovery-assurance/" rel="noopener noreferrer">Governance &amp; Recovery Assurance stage</a> of Rack2Cloud&#8217;s own Data Protection Learning Path is where this kind of decision is supposed to get formalized — a recorded, accountable answer to what recovery may depend on, not a default absorbed silently through a patch cycle.</p>
<p>That&#8217;s a different failure mode from the one <a href="https://www.rack2cloud.com/recovery-evidence-boundary/" rel="noopener noreferrer">Recovery Evidence Boundary</a> describes. Evidence Boundary is about whether anyone structurally independent can validate a recovery claim after the fact. This is upstream of that question — it&#8217;s about whether the organization ever exercised authority over the dependency in the first place. An unvalidated recovery claim is a governance failure at the audit stage. An inherited dependency nobody reviewed is a governance failure at the design stage, and design-stage failures are far cheaper to fix before a feature reaches general availability than after.</p>
<p><a href="https://www.rack2cloud.com/recovery-dependency-mapper/" rel="noopener noreferrer">Recovery Dependency Mapper</a> — tests recovery sequencing against your actual dependency graph and flags cycles or external dependencies nobody documented, including ones an OS vendor added by default.</p>
<h2>
<p>  Architect&#8217;s Verdict<br />
</p></h2>
<p>Cloud-based recovery is not a downgrade. It&#8217;s a different architecture, and most organizations are about to inherit it without ever deciding to.</p>
<p>The real problem isn&#8217;t Microsoft&#8217;s engineering choice — reinstalling from a live, current image is a legitimate improvement over media that goes stale the day it&#8217;s created. The real problem is that a recovery architecture decision is being made by a default setting instead of by the people accountable for recovery outcomes. Every organization running Windows 11 fleets is about to answer &#8220;does OS recovery depend on our internet connection and Microsoft&#8217;s availability&#8221; whether or not anyone ever asked that question on purpose.</p>
<p>An organization that has never reviewed what its recovery depends on doesn&#8217;t have a recovery architecture. It has an assumption that hasn&#8217;t failed yet.</p>
<p>Recovery architecture is not defined by how a system recovers when everything works. It&#8217;s defined by what recovery still depends on when everything else doesn&#8217;t.</p>
<p><em>Originally published at <a href="https://www.rack2cloud.com/cloud-based-recovery-independence/" rel="noopener noreferrer">rack2cloud.com</a></em></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/cloud-based-recovery-didnt-eliminate-usb-drives-it-eliminated-recovery-independence.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Python String Functions</title>
		<link>https://codango.com/python-string-functions.htm</link>
					<comments>https://codango.com/python-string-functions.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 12:07:31 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/python-string-functions.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F4n72grthd69lx3m10re4-li2fii-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />Most data in the real world is string , and python offers a way to interact and transform data to enable us meet our analysis goals. To check Type of <a class="more-link" href="https://codango.com/python-string-functions.htm">Continue reading <span class="screen-reader-text">  Python String Functions</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F4n72grthd69lx3m10re4-li2fii-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>Most data in the real world is string , and python offers a way to interact and transform data to enable us meet our analysis goals. </p>
<h2>
<p>  To check Type of Data<br />
</p></h2>
<p><strong>###1.Types</strong></p>
<h5>
<p>  Type()<br />
</p></h5>
<p>This method enables us to check the type/class of data held in our variables</p>
<p><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%2F4n72grthd69lx3m10re4.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2F4n72grthd69lx3m10re4.png" alt=" " width="799" height="222" /></a></p>
<h5>
<p>  Str()<br />
</p></h5>
<p>This is a built in function that turns any type of data into a string. </p>
<p><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%2Fzvrvimrsvgz0f1i4xemb.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fzvrvimrsvgz0f1i4xemb.png" alt=" " width="800" height="220" /></a><br />
in instances where you dont use f-strings you might to covert your data into text to match the rest of the data type and perform the print function</p>
<p><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%2Fmuq4pd1bx0fmqxw3jpft.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fmuq4pd1bx0fmqxw3jpft.png" alt=" " width="797" height="82" /></a></p>
<p><strong>### Transformations</strong><br />
This methods aim to change the appearance or form of the text</p>
<h5>
<p>  Replace()<br />
</p></h5>
<p>In this you are aiming to replace an existing text with a new value, in you syntax you should specify the old value and the new value / or replace it with nothing</p>
<p><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%2Fw7wpdkniloyo3zwtw310.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fw7wpdkniloyo3zwtw310.png" alt=" " width="792" height="187" /></a></p>
<h5>
<p>  split()<br />
</p></h5>
<p>This aims to split strings to a list , you clarity the item to be used as the separation point and it will be replaced with commas (,)</p>
<p><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%2Fkqvda2lx8uvv0oes8xxm.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fkqvda2lx8uvv0oes8xxm.png" alt=" " width="800" height="134" /></a></p>
<h5>
<p>  Using + /Concat<br />
</p></h5>
<p>We can use the plus sign to join similar data types <br />
<em>&#8220;Hello &#8221; + &#8220;World&#8221; results in &#8220;Hello World&#8221;</em></p>
<h5>
<p>  Join<br />
</p></h5>
<p>Joins turns strings,sets,tuples into a single string, you outline the separator you want to be used <br />
<em>&#8220;SEPARATOR&#8221;.join(list)</em></p>
<p><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%2Flwpfnb4r4m3mq7ykqnot.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Flwpfnb4r4m3mq7ykqnot.png" alt=" " width="800" height="625" /></a></p>
<h5>
<p>  Multiplier<br />
</p></h5>
<p>We use the asterisk that serves as the multiple sign. </p>
<p><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%2F3gl1jmftmk6k7eeyic46.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2F3gl1jmftmk6k7eeyic46.png" alt=" " width="800" height="105" /></a></p>
<h2>
<p>  Cleaning of Whitespaces<br />
</p></h2>
<h5>
<p>  lstrip()<br />
</p></h5>
<p>This methods clears any white space on the left side of the string(<em>leading whitespaces</em>)</p>
<p><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%2Fmbz87gjt1sdyxbmveurf.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fmbz87gjt1sdyxbmveurf.png" alt=" " width="800" height="176" /></a></p>
<h5>
<p>  rstrip<br />
</p></h5>
<p>This method clear whitespace on the right side of the string (<em>trailing spaces</em>)</p>
<p><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%2Fbmf4tjwjnp01dnnwxclg.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fbmf4tjwjnp01dnnwxclg.png" alt=" " width="799" height="189" /></a></p>
<h5>
<p>  strip()<br />
</p></h5>
<p>This method removes leading and trailing white spaces. It doesn&#8217;t change the original string it returns a new string. <br />
<em>Strip()</em> can also be used to remove special characters from the leading and trailing ends only. <br />
Strip can&#8217;t clear white spaces or special characters within the data. </p>
<p><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%2Fe72hdkz3xwg61dvj576e.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fe72hdkz3xwg61dvj576e.png" alt=" " width="799" height="209" /></a></p>
<h2>
<p>  Cleaning Cases<br />
</p></h2>
<h5>
<p>  <em>Lower()</em><br />
</p></h5>
<p>Standardizes the string to lower case</p>
<h5>
<p>  <em>Upper()</em><br />
</p></h5>
<p>Standardizes the string to upper case </p>
<h5>
<p>  <em>Title()</em><br />
</p></h5>
<p>It capitalizes every first letter of all words</p>
<h2>
<p>  Searching<br />
</p></h2>
<h5>
<p>  Startswith()<br />
</p></h5>
<p>This checks the prefix of a string to check for matching and returns either True / False</p>
<p><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%2Fxs9gzhj6czlyupxho90x.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fxs9gzhj6czlyupxho90x.png" alt=" " width="799" height="217" /></a></p>
<h5>
<p>  Endwith()<br />
</p></h5>
<p>This checks the suffix to check for matching and returns a boolean true/false </p>
<p><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%2F26umirmwouwwefb4v04s.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2F26umirmwouwwefb4v04s.png" alt=" " width="800" height="209" /></a></p>
<h5>
<p>  Find<br />
</p></h5>
<p>This helps us find the index of the first occurrence of the substring, it returns -1 incase the substring isn&#8217;t found </p>
<h5>
<p>  Integer<br />
</p></h5>
<p>This checks the position of a substring in a string, performs similar to <em>find</em> but the difference is that if the substring doesn&#8217;t exist it will crash . <br />
The safe option is using find because incase the substring doesn&#8217;t exist it gives a -1 feedback </p>
<p><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%2Fkj7ubckd0ls76cwvuivw.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fkj7ubckd0ls76cwvuivw.png" alt=" " width="800" height="219" /></a></p>
<h5>
<p>  In operator<br />
</p></h5>
<p><em>In</em> check if a value exists in a list, tuple, string<br />
It returns True if found and False if the substring doesn&#8217;t exist</p>
<p><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%2F2ut0tzj2e6abft28xhfm.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2F2ut0tzj2e6abft28xhfm.png" alt=" " width="777" height="117" /></a></p>
<h2>
<p>  Validation<br />
</p></h2>
<p>This methods check if the strings meet certain conditions. <br />
The feedback is boolean , either True or False </p>
<p><em>text.isalpha()</em>       Only letters<br />
<em>text.isdigit()</em>       Only digits<br />
<em>text.isalnum()</em>       Letters and numbers only<br />
<em>text.isnumeric()</em>     Numeric characters<br />
<em>text.isspace()</em>       Only spaces<br />
<em>text.islower()</em>       All letters are lowercase<br />
<em>text.isupper()</em>       All letters are uppercase<br />
<em>text.istitle()</em>       Each word starts with uppercase</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/python-string-functions.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Web Mimarisi: Gelecek Şimdi</title>
		<link>https://codango.com/web-mimarisi-gelecek-simdi.htm</link>
					<comments>https://codango.com/web-mimarisi-gelecek-simdi.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 11:33:33 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/web-mimarisi-gelecek-simdi/</guid>

					<description><![CDATA[Yaz tatili birçok kişi için dinlenmek, gezmek ve yeni yerler keşfetmek demektir. Ancak bir grup kişi için tatil, yeni projeler geliştirmek ve yaratıcılıklarını ortaya koymak için eşsiz bir fırsat anlamına <a class="more-link" href="https://codango.com/web-mimarisi-gelecek-simdi.htm">Continue reading <span class="screen-reader-text">  Web Mimarisi: Gelecek Şimdi</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Yaz tatili birçok kişi için dinlenmek, gezmek ve yeni yerler keşfetmek demektir. Ancak bir grup kişi için tatil, yeni projeler geliştirmek ve yaratıcılıklarını ortaya koymak için eşsiz bir fırsat anlamına gelebilir. İşte bu yazının amacı da, web mimarisi alanında yenilikçi fikirler ve uygulamalar geliştirmek isteyenlere ilham vermek ve yol göstermek.</p>
<p>Web mimarisi, web uygulamalarının ve web sitelerinin tasarım ve yapısını tanımlayan kritik bir alandır. Kullanıcı deneyimini, performansını ve ölçeklenebilirliğini doğrudan etkileyen bir konu olarak, web mimarisi alanında yapılan yenilikler ve gelişmeler, web dünyasının geleceğini şekillendiriyor. Bu yazıda, web mimarisi alanındaki son trendleri, en iyi uygulamaları ve geleceğin web uygulamalarını oluşturmak için kullanılan ileri düzey teknikleri keşfederek web geliştiricilerinin ufuklarını genişletmeyi hedefliyoruz.</p>
<h2>
<p>  Geleceğin Web Mimarisi: Mikro Hizmetler ve Sunucusuz Mimari<br />
</p></h2>
<p>Modern web uygulamaları, artan kullanıcı talepleri ve karmaşıklık nedeniyle geleneksel tek parça mimarilerin ötesine geçmeyi gerektiriyor. Mikro hizmetler ve sunucusuz mimari, web uygulamalarının ölçeklenebilirliğini ve bakımını kolaylaştırmak için ortaya çıkan popüler yaklaşımlar haline geldi.</p>
<h3>
<p>  Mikro Hizmetler Mimarisi<br />
</p></h3>
<p>Mikro hizmetler mimarisi, bir uygulamayı daha küçük, bağımsız hizmetlere bölerek uygulamayı modüler hale getirir. Her mikro hizmet, belirli bir iş işlevini yerine getirir ve kendi veri depolarını ve arabirimlerini kullanır. Bu yaklaşım, her hizmetin bağımsız olarak geliştirilmesine, test edilmesine ve dağıtılmasına olanak tanır, böylece geliştirme sürecini hızlandırır ve esneklik sağlar.</p>
<p>Örneğin, bir e-ticaret web sitesi, ürün kataloğu, alışveriş sepeti, ödeme işlemleri ve müşteri hesapları gibi farklı mikro hizmetlerden oluşabilir. Bu mikro hizmetler birbirleriyle iletişim kurarak sorunsuz bir kullanıcı deneyimi sağlar. Mikro hizmetler mimarisi, her hizmetin ayrı olarak ölçeklendirilebilmesini sağlayarak uygulamanın genel performansını artırır.</p>
<h3>
<p>  Sunucusuz Mimari<br />
</p></h3>
<p>Sunucusuz mimari, sunucu yönetim yükünü ortadan kaldıran ve kod yürütme için ölçeklenebilir bir ortam sağlayan bir yaklaşımdır. Bu mimaride, işlevsel kod parçaları (&#8220;fonksiyonlar&#8221;) bulut sağlayıcıları tarafından yönetilen ortamlarda çalıştırılır. Sunucusuz mimari, kodun yalnızca gerektiğinde yürütülmesini ve dinamik olarak ölçeklendirilmesini sağlar, böylece maliyet verimliliği ve esneklik sağlanır.</p>
<p>Örneğin, bir web uygulaması, kullanıcı kayıt işlemini işlemek için sunucusuz fonksiyonlar kullanabilir. Kullanıcı kaydolduğunda, sunucusuz fonksiyon tetiklenir, gerekli doğrulamaları yapar ve kullanıcı verilerini bir veritabanına kaydeder. Bu yaklaşım, sunucu yönetimiyle uğraşmadan esnek ve ekonomik bir çözüm sunar.</p>
<h2>
<p>  Uygulama: Reaktif Mimari ile Gerçek Zamanlı Web Uygulamaları<br />
</p></h2>
<p>Reaktif mimari, web uygulamalarına gerçek zamanlı ve etkileşimli özellikler katmak için kullanılan modern bir yaklaşımdır. Reaktif mimari, veri akışlarını ve değişiklikleri yöneterek dinamik ve yanıt veren kullanıcı arabirimleri oluşturmayı amaçlar.</p>
<h3>
<p>  React ve Angular ile Reaktif Kullanıcı Arabirimleri<br />
</p></h3>
<p>React ve Angular, reaktif mimariyi benimseyen popüler frontend kütüphaneleridir. Bu kütüphaneler, kullanıcı arabirimlerini küçük bileşenlere bölerek ve veri akışını yöneterek dinamik ve etkileşimli kullanıcı deneyimleri oluşturmayı kolaylaştırır.</p>
<p>Örneğin, bir sosyal medya akışını gösteren bir web uygulaması düşünün. React veya Angular kullanarak, yeni gönderiler alındıkça dinamik olarak güncellenen bir akış bileşeni oluşturabilirsiniz. Bu kütüphaneler, verilerde yapılan değişikliklere yanıt veren ve kullanıcı arabirimini buna göre güncelleyen reaktif veri akışları sağlar.</p>
<p><strong>Kod Örneği: React ile Reaktif Kullanıcı Arabirimi</strong>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight jsx"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nf">SocialMediaFeed</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">posts</span><span class="p">,</span> <span class="nx">setPosts</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">([]);</span>

  <span class="kd">const</span> <span class="nx">handleNewPost</span> <span class="o">=</span> <span class="p">(</span><span class="nx">newPost</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">setPosts</span><span class="p">([...</span><span class="nx">posts</span><span class="p">,</span> <span class="nx">newPost</span><span class="p">]);</span>
  <span class="p">};</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Social Media Feed<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="si">{</span><span class="nx">posts</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">post</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
          <span class="p">&lt;</span><span class="nc">Post</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">post</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span> <span class="na">post</span><span class="p">=</span><span class="si">{</span><span class="nx">post</span><span class="si">}</span> <span class="p">/&gt;</span>
        <span class="p">))</span><span class="si">}</span>
      <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nf">Post</span><span class="p">({</span> <span class="nx">post</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">post</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">post</span><span class="p">.</span><span class="nx">content</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">SocialMediaFeed</span><span class="p">;</span>
</code></pre>
</div>
<p>Bu kod örneğinde, <code>SocialMediaFeed</code> bileşeni, <code>posts</code> adlı bir durum değişkeni ve <code>handleNewPost</code> adlı bir fonksiyon tanımlar. Yeni bir gönderi alındığında, <code>handleNewPost</code> fonksiyonu tetiklenir ve yeni gönderi mevcut gönderilerin listesine eklenir. <code>posts</code> listesi, <code>map</code> fonksiyonu kullanılarak <code>Post</code> bileşenine geçirilir ve her gönderi ayrı ayrı işlenir.</p>
<p>React&#8217;in reaktif doğası, <code>posts</code> listesindeki değişiklikleri algılar ve kullanıcı arabirimini otomatik olarak günceller, böylece yeni gönderiler dinamik olarak akışa eklenir. Bu yaklaşım, gerçek zamanlı ve etkileşimli bir kullanıcı deneyimi sağlar.</p>
<h2>
<p>  Geleceğin Web Mimarisi için Bulut Hizmetleri<br />
</p></h2>
<p>Bulut hizmetleri, web mimarisi alanındaki yenilikleri hızlandırmada kritik bir rol oynamaktadır. AWS, Azure ve Google Cloud gibi bulut sağlayıcıları, ölçeklenebilirlik, güvenlik ve esneklik sunan çeşitli hizmetler sunar.</p>
<ul>
<li>
<strong>Bulut Depolama</strong>: AWS S3 veya Azure Blob Depolama gibi bulut depolama hizmetleri, büyük miktarda veri depolamak ve erişmek için kullanılır. Web uygulamalarının statik içeriklerini, kullanıcı yüklerini veya yedeklemelerini depolamak için idealdir.</li>
<li>
<strong>Bulut İşlevleri</strong>: AWS Lambda veya Azure İşlevleri gibi sunucusuz hizmetler, kod yürütme için ölçeklenebilir bir ortam sağlar. Web uygulamalarının arka uç mantığını çalıştırmak için idealdir ve sunucu yönetimi yükünü ortadan kaldırır.</li>
<li>
<strong>Bulut Veritabanları</strong>: MongoDB Atlas veya AWS RDS gibi yönetilen veritabanı hizmetleri, ölçeklenebilir ve güvenilir veri depolama sağlar. Web uygulamalarının verilerini güvenli bir şekilde depolamak ve sorgulamak için kullanılabilir.</li>
<li>
<strong>İçerik Dağıtım Ağları (CDN&#8217;ler)</strong>: CDN&#8217;ler, web uygulamalarının statik içeriklerini küresel olarak dağıtarak performansını artırır. Kullanıcıya yakın konumlardan içerik sunulması sayfa yükleme sürelerini kısaltır.</li>
</ul>
<h2>
<p>  Sonuç<br />
</p></h2>
<p>Web mimarisi, web uygulamalarının temelini oluşturan kritik bir alandır ve sürekli gelişen yenilikler ve trendler sunmaktadır. Mikro hizmetler mimarisi, sunucusuz mimari ve reaktif mimari gibi yaklaşımlar, web uygulamalarının ölçeklenebilirliğini, esnekliğini ve etkileşimliliğini artırıyor.</p>
<p>Ayrıca, bulut hizmetleri, web mimarisi alanındaki yenilikleri destekleyen güçlü araçlar sunmaktadır. Bulut depolama, sunucusuz işlevler ve CDN&#8217;ler, web uygulamalarının performansını, güvenilirliğini ve bakımını geliştirir.</p>
<p>Web geliştiricileri olarak, bu ileri düzey teknikleri ve yaklaşımları benimseyerek geleceğin web uygulamalarını oluşturabilir ve kullanıcılara etkileyici ve yenilikçi deneyimler sunabiliriz. Web mimarisi alanındaki sürekli gelişme, web dünyasının geleceğini şekillendirmeye devam edecek.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/web-mimarisi-gelecek-simdi.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Adhiya Meets Athena: Exploring Amazon Athena &#x1f50d;&#x2601;&#xfe0f;</title>
		<link>https://codango.com/adhiya-meets-athena-exploring-amazon-athena-%f0%9f%94%8d%e2%98%81%ef%b8%8f.htm</link>
					<comments>https://codango.com/adhiya-meets-athena-exploring-amazon-athena-%f0%9f%94%8d%e2%98%81%ef%b8%8f.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 01:55:04 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/adhiya-meets-athena-exploring-amazon-athena-%f0%9f%94%8d%e2%98%81%ef%b8%8f.htm</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Frl4nbanb15wicht201tx-iyiqMe-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />Introduction: As students, we work with data almost every day. Whether it is a machine learning dataset, college records, project logs, or survey results, analyzing large amounts of data can <a class="more-link" href="https://codango.com/adhiya-meets-athena-exploring-amazon-athena-%f0%9f%94%8d%e2%98%81%ef%b8%8f.htm">Continue reading <span class="screen-reader-text">  Adhiya Meets Athena: Exploring Amazon Athena &#x1f50d;&#x2601;&#xfe0f;</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Frl4nbanb15wicht201tx-iyiqMe-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p><strong>Introduction</strong>:</p>
<p>As students, we work with data almost every day. Whether it is a machine learning dataset, college records, project logs, or survey results, analyzing large amounts of data can become difficult when everything has to be downloaded and processed locally.</p>
<p>While learning AWS, I came across <strong>Amazon Athena</strong>, a service that makes this process much simpler.</p>
<p>Amazon Athena is a <strong>serverless interactive query service</strong> that allows us to analyze data stored in Amazon S3 using standard SQL. The interesting part is that we do not need to set up or manage servers, clusters, or other infrastructure before running queries.</p>
<p>In this blog, I will explain Amazon Athena in simple terms, how it works, its important features, a practical student use case, and some things to consider before using it.</p>
<p><strong>What is Amazon Athena?</strong></p>
<p><strong>Amazon Athena</strong> is a serverless analytics service from AWS.</p>
<p>Its main purpose is to allow users to query data directly where it is stored, particularly in <strong>Amazon S3</strong>, using SQL.</p>
<p>For example, imagine that I have a large CSV file containing student project information:</p>
<p>student_id,department,project,score<br />
101,AI&amp;ML,RenalScan,92<br />
102,CSE,SmartCampus,87<br />
103,AI&amp;ML,ChurnGuard,90</p>
<p>Instead of downloading the entire dataset and processing it on my laptop, I can store the file in S3 and use Athena to query it.</p>
<p>For example:</p>
<p>sql<br />
SELECT department, AVG(score)<br />
FROM student_projects<br />
GROUP BY department;</p>
<p>Athena processes the query and returns the result.</p>
<p>It also supports multiple common data formats, including CSV, JSON, ORC, Avro, and Apache Parquet.</p>
<p><strong>Why Was Amazon Athena Created?</strong></p>
<p>Analyzing large datasets traditionally required infrastructure such as servers, database systems, or data-processing clusters.</p>
<p>For students and smaller teams, setting up and maintaining this infrastructure can be unnecessary for many analytics tasks.</p>
<p>Amazon Athena provides a serverless approach. We can keep data in Amazon S3, define its schema, and start running SQL queries without managing the underlying servers.</p>
<p>This makes Athena particularly useful for <strong>ad-hoc analysis, log analysis, data exploration, and querying data lakes</strong>.</p>
<p>The main idea is simple:</p>
<blockquote>
<p>Store the data in the cloud and query it when you need it.</p>
</blockquote>
<p><strong>How Does Amazon Athena Work?</strong></p>
<p>The basic workflow looks like this:</p>
<p><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%2Frl4nbanb15wicht201tx.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Frl4nbanb15wicht201tx.png" alt=" " width="510" height="609" /></a></p>
<p>The process can be explained in a few steps:</p>
<ol>
<li>Store the dataset in an Amazon S3 bucket.</li>
<li>Open Amazon Athena.</li>
<li>Define the structure/schema of the data.</li>
<li>Write a SQL query.</li>
<li>Athena reads the required data from S3.</li>
<li>The query is processed without the user managing servers.</li>
<li>Results are displayed and can also be stored in S3.</li>
</ol>
<p>Athena is designed to execute queries in parallel and automatically scale the query-processing infrastructure.</p>
<p><strong>Key Features of Amazon Athena</strong></p>
<p><strong>1.Serverless Architecture</strong></p>
<p>The biggest feature of Athena is that it is <strong>serverless</strong>.</p>
<p>There is no need to create or maintain EC2 instances, configure clusters, install database software, or manually handle scaling.</p>
<p>AWS manages the infrastructure required to execute the queries, allowing the user to focus mainly on the data and SQL.</p>
<p>For a student, this is useful because we can experiment with analytics without first learning how to manage a complete data-processing cluster.</p>
<p><strong>2.Query Data Using Standard SQL</strong></p>
<p>Athena allows users to analyze data using SQL.</p>
<p>For someone who already knows SQL through a DBMS course, this makes Athena relatively approachable.</p>
<p>For example:</p>
<p>sql<br />
SELECT project, AVG(score) AS average_score<br />
FROM student_projects<br />
GROUP BY project<br />
ORDER BY average_score DESC;</p>
<p>Athena supports SQL operations such as joins, window functions, and arrays, and supports several data formats including CSV, JSON, ORC, Avro, and Parquet.</p>
<p><strong>3.Integration with Amazon S3</strong></p>
<p>Athena works directly with data stored in Amazon S3.</p>
<p>We do not have to first move the entire dataset into a traditional database before querying it.</p>
<p>This makes the combination of <strong>S3 + Athena</strong> useful for building a simple data lake architecture.</p>
<p><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%2Fqoj3ccfbhaetxhitqrx0.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fqoj3ccfbhaetxhitqrx0.png" alt=" " width="408" height="603" /></a></p>
<p>Athena can also use the AWS Glue Data Catalog to help define and manage table and partition metadata.</p>
<p><strong>4.Workgroups for Managing Queries</strong></p>
<p>Athena provides <strong>workgroups</strong> that can be used to separate workloads and teams.</p>
<p>For example, a college could create separate workgroups for:</p>
<p><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%2Fz075x5z7xou46h3ucjld.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" 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%2Fz075x5z7xou46h3ucjld.png" alt=" " width="800" height="519" /></a></p>
<p>Workgroups can also be used to control access, configure query-result locations, monitor usage, and establish data-usage limits.</p>
<p><strong>College / Student Use Case <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f393.png" alt="🎓" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong></p>
<p>A practical use case for my college would be a <strong>Student Project Analytics System</strong>.</p>
<p>Suppose a department has thousands of project records containing:</p>
<ul>
<li>Student ID</li>
<li>Department</li>
<li>Project title</li>
<li>Technology used</li>
<li>Project score</li>
<li>Academic year</li>
<li>Project category</li>
</ul>
<p>These records could be stored in S3.</p>
<p>Athena could then be used to answer questions such as:</p>
<p>sql<br />
SELECT technology, COUNT(*) AS project_count<br />
FROM student_projects<br />
GROUP BY technology<br />
ORDER BY project_count DESC;</p>
<p>The department could use the results to understand which technologies students are using most frequently.</p>
<p>Another query could calculate the average project score:</p>
<p>sql<br />
SELECT department, AVG(score) AS average_score<br />
FROM student_projects<br />
GROUP BY department;</p>
<p>This would allow faculty members to analyze project trends without building and maintaining a dedicated database server for every analysis task.</p>
<p><strong>A Simple Practical Example</strong></p>
<p>Imagine that I have uploaded a CSV file called:<br />
student_projects.csv<br />
to an S3 bucket.</p>
<p>After defining the table structure in Athena, I can run:</p>
<p>sql<br />
SELECT *<br />
FROM student_projects<br />
LIMIT 10;</p>
<p>To find the number of projects in each department:</p>
<p>sql<br />
SELECT department, COUNT(*) AS total_projects<br />
FROM student_projects<br />
GROUP BY department;</p>
<p>To find high-scoring projects:</p>
<p>sql<br />
SELECT student_id, project, score<br />
FROM student_projects<br />
WHERE score &gt;= 90<br />
ORDER BY score DESC;</p>
<p>The important point is that these queries can be run against data stored in S3 rather than requiring me to download the entire dataset onto my computer.</p>
<p>Athena can be accessed through the AWS Management Console, API, AWS CLI, SDKs, and supported JDBC/ODBC connections.</p>
<p><strong>Advantages of Amazon Athena:</strong></p>
<p><strong>No Server Management</strong></p>
<p>Athena is serverless, so users do not need to manage servers, clusters, software updates, or infrastructure scaling.</p>
<p><strong>Easy for SQL Users</strong></p>
<p>Anyone familiar with SQL can start exploring datasets without learning an entirely new query language.</p>
<p><strong>Scalable</strong></p>
<p>Athena is designed to automatically scale query execution and process queries in parallel.</p>
<p><strong>Works with Large Datasets</strong></p>
<p>Athena can be used for interactive analysis of large datasets stored in S3.</p>
<p><strong>Integration with AWS</strong></p>
<p>It works with services and tools such as Amazon S3, AWS Glue Data Catalog, IAM, CloudWatch, and business intelligence tools.</p>
<p><strong>Limitations and Things to Consider:</strong></p>
<p><strong>Cost</strong></p>
<p>Athena is not simply a completely free service.</p>
<p>For SQL queries, the default pricing model is based on the amount of data scanned. AWS also provides capacity-based pricing for certain workloads.</p>
<p>This means poorly optimized queries over very large datasets can become expensive.</p>
<p>Using <strong>compression, partitioning, and columnar formats such as Parquet</strong> can reduce the amount of data scanned and therefore reduce query costs.</p>
<p><strong>Complexity</strong></p>
<p>Basic queries are easy, but working with large datasets requires knowledge of data formats, schemas, partitions, permissions, and query optimization.</p>
<p><strong>Scalability</strong></p>
<p>Athena itself scales automatically, but the way data is stored still matters. Poorly organized datasets can result in unnecessary data scanning and slower or more expensive queries.</p>
<p><strong>Security</strong></p>
<p>Data access needs to be configured carefully. Athena works with AWS IAM policies and Amazon S3 bucket policies to control who can access the underlying data.</p>
<p>Athena can also query encrypted data stored in S3 and can encrypt query results.</p>
<p><strong>Conclusion:</strong></p>
<p>Amazon Athena is a useful AWS service for anyone who needs to analyze large amounts of data without managing servers.</p>
<p>Its combination of <strong>serverless architecture, SQL support, S3 integration, scalability, and flexible query management</strong> makes it useful for students as well as organizations.</p>
<p>For a student like me, Athena is especially interesting because it connects concepts that we already learn in college—such as SQL, databases, data analytics, and cloud computing.</p>
<p>A simple combination of <strong>Amazon S3 + Amazon Athena</strong> can turn a collection of raw datasets into something that can be queried and analyzed using familiar SQL commands.</p>
<p>My biggest takeaway from learning about Athena is that cloud computing is not always about managing more infrastructure. Sometimes, the advantage is that <strong>AWS manages the infrastructure so we can concentrate on solving the actual data problem.</strong> <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><strong>References</strong></p>
<ul>
<li><a href="https://aws.amazon.com/athena/?utm_source=chatgpt.com" rel="noopener noreferrer">Amazon Athena – AWS Official Overview</a></li>
<li><a href="https://docs.aws.amazon.com/athena/?utm_source=chatgpt.com" rel="noopener noreferrer">Amazon Athena Documentation</a></li>
<li><a href="https://aws.amazon.com/athena/features/?utm_source=chatgpt.com" rel="noopener noreferrer">Amazon Athena Features</a></li>
<li><a href="https://aws.amazon.com/athena/pricing/?utm_source=chatgpt.com" rel="noopener noreferrer">Amazon Athena Pricing</a></li>
<li><a href="https://aws.amazon.com/athena/faqs/?utm_source=chatgpt.com" rel="noopener noreferrer">Amazon Athena FAQs</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/adhiya-meets-athena-exploring-amazon-athena-%f0%9f%94%8d%e2%98%81%ef%b8%8f.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Thonburian Whisper ถอดเสียงไทยในเครื่องฟรี ใช้ฟรีแม้เชิงพาณิชย์</title>
		<link>https://codango.com/thonburian-whisper-%e0%b8%96%e0%b8%ad%e0%b8%94%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99%e0%b9%80%e0%b8%84%e0%b8%a3%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%87.htm</link>
					<comments>https://codango.com/thonburian-whisper-%e0%b8%96%e0%b8%ad%e0%b8%94%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99%e0%b9%80%e0%b8%84%e0%b8%a3%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%87.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 01:48:59 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/thonburian-whisper-%e0%b8%96%e0%b8%ad%e0%b8%94%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99%e0%b9%80%e0%b8%84%e0%b8%a3%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%87.htm</guid>

					<description><![CDATA[Thonburian Whisper ถอดเสียงไทยในเครื่องฟรี ใช้ฟรีแม้เชิงพาณิชย์ โดย Nokka (นก-กา) &#124; 15 กันยายน 2026 บทความนี้เขียนโดย AI (โมเดล deepseek-v4.1-flash ของผู้ให้บริการ ollama-cloud) ผ่าน Hermes Agent จาก Nous Research ตรวจสอบและเรียบเรียงโดย Nokka เวลาพูดถึง AI กับเสียงไทย คนมักนึกถึง เสียงสังเคราะห์ <a class="more-link" href="https://codango.com/thonburian-whisper-%e0%b8%96%e0%b8%ad%e0%b8%94%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99%e0%b9%80%e0%b8%84%e0%b8%a3%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%87.htm">Continue reading <span class="screen-reader-text">  Thonburian Whisper ถอดเสียงไทยในเครื่องฟรี ใช้ฟรีแม้เชิงพาณิชย์</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h1>
<p>  Thonburian Whisper ถอดเสียงไทยในเครื่องฟรี ใช้ฟรีแม้เชิงพาณิชย์<br />
</p></h1>
<p><em>โดย Nokka (นก-กา) | 15 กันยายน 2026</em></p>
<p><em>บทความนี้เขียนโดย AI (โมเดล deepseek-v4.1-flash ของผู้ให้บริการ ollama-cloud) ผ่าน Hermes Agent จาก Nous Research ตรวจสอบและเรียบเรียงโดย Nokka</em></p>
<p>เวลาพูดถึง AI กับเสียงไทย คนมักนึกถึง <strong>เสียงสังเคราะห์</strong> คือทำให้ AI พูด</p>
<p><strong>แต่อีกด้านหนึ่งสำคัญพอ ๆ กัน และมีคนเขียนถึงน้อยกว่ามาก</strong> คือ <strong>การถอดเสียงพูดไทยเป็นข้อความ</strong> [1]</p>
<p><strong>Thonburian Whisper</strong> คือชุดโมเดลที่ทำเรื่องนี้ จากทีม Biomedical and Data Lab มหาวิทยาลัยมหิดล <strong>เปิดให้ใช้ฟรีและใช้เชิงพาณิชย์ได้</strong> [1][2]</p>
<h2>
<p>  Thonburian Whisper คืออะไร<br />
</p></h2>
<p>เป็นชุดโมเดล <strong>Automatic Speech Recognition (ASR)</strong> ที่ <strong>fine-tune จาก Whisper ของ OpenAI</strong> สำหรับภาษาไทยโดยเฉพาะ [1][2]</p>
<p><strong>ทีมเผยแพร่หลายขนาดให้เลือกตามทรัพยากรที่มี</strong></p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>โมเดล</th>
<th>ฐาน</th>
<th>Downloads บน Hugging Face</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>whisper-th-medium-combined</code></td>
<td>whisper-medium</td>
<td><strong>4,405</strong></td>
</tr>
<tr>
<td><code>whisper-th-large-v3-combined</code></td>
<td>whisper-large-v3</td>
<td><strong>1,672</strong></td>
</tr>
<tr>
<td><code>distill-whisper-th-small</code></td>
<td>distill จาก small</td>
<td>1,107</td>
</tr>
<tr>
<td><code>whisper-th-small-combined</code></td>
<td>whisper-small</td>
<td>1,074</td>
</tr>
<tr>
<td><code>whisper-th-large-combined</code></td>
<td>whisper-large</td>
<td>556</td>
</tr>
<tr>
<td><code>distill-whisper-th-large-v3</code></td>
<td>distill จาก large-v3</td>
<td>354</td>
</tr>
</tbody>
</table>
</div>
<p><strong>ตัวเลข downloads ด้านบนผมดึงจาก Hugging Face API โดยตรง</strong> ณ วันที่เขียน [2] </p>
<h2>
<p>  คะแนน WER และความต่างที่ราคาแลกมา<br />
</p></h2>
<p><strong>WER (Word Error Rate)</strong> คือสัดส่วนคำที่ถอดผิด ยิ่งต่ำยิ่งดี [1]</p>
<p>ตัวเลข WER ทั้งสองตัวด้านล่างผมดึงจากหน้าโมเดลทางการบน Hugging Face ทั้ง<a href="https://huggingface.co/biodatlab/whisper-th-medium-combined" rel="noopener noreferrer">รุ่น medium</a> [1] และ<a href="https://huggingface.co/biodatlab/whisper-th-large-v3-combined" rel="noopener noreferrer">รุ่น large-v3</a> [3]</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>โมเดล</th>
<th>WER</th>
<th>ขนาด</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>whisper-th-large-v3-combined</code></td>
<td><strong>6.59</strong></td>
<td>ใหญ่สุด</td>
</tr>
<tr>
<td><code>whisper-th-medium-combined</code></td>
<td><strong>7.42</strong></td>
<td>กลาง</td>
</tr>
<tr>
<td><code>distill-whisper-th-small</code></td>
<td>ทีมไม่ได้ระบุในหน้าโมเดล</td>
<td>เล็ก</td>
</tr>
</tbody>
</table>
</div>
<p><strong>ทั้งสองค่าที่ระบุวัดบนชุดทดสอบ <code>common_voice_13_0</code> ภาษาไทย ซึ่งระบุไว้บน<a href="https://huggingface.co/biodatlab/whisper-th-medium-combined" rel="noopener noreferrer">หน้าโมเดลทางการ</a> [1] และใช้ Deepcut tokenizer ในการตัดคำ</strong> ซึ่งเป็นรายละเอียดที่ผมคิดว่าสำคัญ เพราะ <strong>ค่า WER ภาษาไทยขึ้นกับวิธีตัดคำเป็นอย่างมาก</strong> [1]</p>
<p><strong>ผมต้องชี้ให้ชัดตรงนี้</strong> ตัวเลข 6.59 กับ 7.42 <strong>วัดด้วยวิธีเดียวกัน</strong> จึงเทียบกันได้ <strong>แต่ผมไม่สามารถบอกได้ว่าเทียบกับโมเดลอื่นได้หรือไม่</strong> เพราะแต่ละทีมใช้ชุดทดสอบและตัวตัดคำต่างกัน [1]</p>
<h2>
<p>  ทำไมผมคิดว่าตัวนี้คุ้มค่าที่จะรู้จัก<br />
</p></h2>
<p><strong>หนึ่ง: ใบอนุญาต Apache 2.0</strong> [1]</p>
<p>ผมตรวจจากหน้าโมเดลทั้งสองตัว (medium-combined และ large-v3-combined) ระบุ <strong><code>license: apache-2.0</code></strong> เหมือนกัน [1]</p>
<p><strong>ข้อนี้สำคัญเพราะต่างจากโมเดลเสียงไทยหลายตัวที่ห้ามใช้เชิงพาณิชย์</strong> ⇒ ถ้าคุณทำแอปหรือบริการที่ต้องถอดเสียงลูกค้า ตัวนี้ใช้ได้โดยไม่ต้องขออนุญาต [1]</p>
<p><strong>สอง: มีให้เลือกหลายขนาดจนรันในเครื่องได้จริง</strong></p>
<p>มีตั้งแต่รุ่น small ที่รันบนเครื่องทั่วไป ไปจนถึง large-v3 ที่ต้องใช้ GPU แต่ให้คะแนนดีที่สุด [1][2]</p>
<p><strong>สาม: มีรุ่นกลั่น (distill) สำหรับคนที่ต้องการความเร็ว ซึ่ง<a href="https://huggingface.co/biodatlab/distill-whisper-th-large-v3" rel="noopener noreferrer">ทีมเผยแพร่แยกไว้ต่างหาก</a> [4]</strong></p>
<p><code>distill-whisper-th-small</code> และ <code>distill-whisper-th-large-v3</code> [4] เป็นรุ่นที่กลั่นให้เล็กลงและเร็วขึ้น ซึ่งมักแลกกับความแม่นบ้าง [2]</p>
<h2>
<p>  วิธีใช้ห้าบรรทัดจบ<br />
</p></h2>
<p>ทีมให้โค้ดตัวอย่างไว้บนหน้าโมเดล และเป็นวิธีมาตรฐานของ Whisper [1]
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">transformers</span> <span class="kn">import</span> <span class="n">pipeline</span>

<span class="n">MODEL_NAME</span> <span class="o">=</span> <span class="sh">"</span><span class="s">biodatlab/whisper-th-medium-combined</span><span class="sh">"</span>
<span class="n">lang</span> <span class="o">=</span> <span class="sh">"</span><span class="s">th</span><span class="sh">"</span>

<span class="n">device</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="nf">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="sh">"</span><span class="s">cpu</span><span class="sh">"</span>

<span class="n">pipe</span> <span class="o">=</span> <span class="nf">pipeline</span><span class="p">(</span>
    <span class="n">task</span><span class="o">=</span><span class="sh">"</span><span class="s">automatic-speech-recognition</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="n">MODEL_NAME</span><span class="p">,</span>
    <span class="n">chunk_length_s</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>
    <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">pipe</span><span class="p">.</span><span class="n">model</span><span class="p">.</span><span class="n">config</span><span class="p">.</span><span class="n">forced_decoder_ids</span> <span class="o">=</span> <span class="n">pipe</span><span class="p">.</span><span class="n">tokenizer</span><span class="p">.</span><span class="nf">get_decoder_prompt_ids</span><span class="p">(</span>
  <span class="n">language</span><span class="o">=</span><span class="n">lang</span><span class="p">,</span>
  <span class="n">task</span><span class="o">=</span><span class="sh">"</span><span class="s">transcribe</span><span class="sh">"</span>
<span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="nf">pipe</span><span class="p">(</span><span class="sh">"</span><span class="s">audio.mp3</span><span class="sh">"</span><span class="p">)[</span><span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">]</span>
</code></pre>
</div>
<p><strong>จุดที่ผมคิดว่าคนมักลืมคือสองบรรทัดกลาง</strong> การตั้ง <code>forced_decoder_ids</code> ให้ภาษาเป็น <code>th</code> และ task เป็น <code>transcribe</code> ถ้าไม่ตั้ง โมเดลอาจถอดเป็นภาษาอังกฤษหรือแปลแทนที่จะถอด [1]</p>
<h3>
<p>  ประวัติ: เริ่มจากปี 2022<br />
</p></h3>
<p>ทีมเผยแพร่ชุดนี้ครั้งแรกเมื่อปี <strong>2022</strong> โดย Atirut Boribalburephan · Zaw Htet Aung · Knot Pipatsrisawat · Titipat Achakulvisut [1]</p>
<p><strong>การอ้างอิงใน BibTeX ของทีมระบุปี 2022</strong> และมี DOI บน Hugging Face [1]</p>
<p><strong>แต่รุ่นที่คนใช้มากที่สุดในตอนนี้ (<code>whisper-th-medium-combined</code>) เป็นรุ่นที่ฝึกต่อมาแล้ว</strong> หน้าโมเดลระบุว่า <strong>Combined V4</strong> ฝึกบนข้อมูล augmented จาก common_voice_13_0 ภาษาไทย · google/fleurs และชุดข้อมูลที่ทีมคัดสรรเพิ่ม [1]</p>
<p><strong>ผมต้องบอกตรง ๆ ว่า</strong> ผมไม่พบวันที่เผยแพร่ที่ชัดเจนของรุ่น V4 สิ่งที่ยืนยันได้คือ <strong>BibTeX ระบุปี 2022</strong> ซึ่งเป็นปีของเวอร์ชันแรก [1]</p>
<h3>
<p>  พารามิเตอร์การฝึกที่ทีมเปิดไว้<br />
</p></h3>
<p>หน้าโมเดลของรุ่น medium ระบุรายละเอียดการฝึกไว้ครบ [1]</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>พารามิเตอร์</th>
<th>ค่า</th>
</tr>
</thead>
<tbody>
<tr>
<td>learning rate</td>
<td>1e-05</td>
</tr>
<tr>
<td>train batch size</td>
<td>16</td>
</tr>
<tr>
<td>training steps</td>
<td>10,000</td>
</tr>
<tr>
<td>optimizer</td>
<td>AdamW (betas 0.9, 0.999)</td>
</tr>
<tr>
<td>lr scheduler</td>
<td>linear พร้อม warmup 500 steps</td>
</tr>
<tr>
<td>mixed precision</td>
<td>Native AMP</td>
</tr>
</tbody>
</table>
</div>
<p><strong>ทีมเปิดไว้ให้ดูทั้งหมด</strong> ซึ่งเป็นเรื่องที่มีคุณค่าในความเห็นของผม เพราะทำให้คนที่อยากฝึกต่อยอดรู้ว่าต้องเริ่มจากจุดไหน [1]</p>
<h2>
<p>  เรื่องที่ทีมยังไม่ได้เขียนไว้<br />
</p></h2>
<p>หน้าโมเดลทั้งสองรุ่นของทีม <strong>ระบุไว้ตรง ๆ ว่า *&#8221;Intended uses &amp; limitations: More information needed&#8221;</strong>* และ <em>&#8220;Training and evaluation data: More information needed&#8221;</em> [1]</p>
<p><strong>ผมจึงไม่เดาว่าข้อจำกัดของโมเดลคืออะไร</strong> แต่สิ่งที่ผม <strong>ยืนยันได้</strong> คือทีมเปิดเผยว่ายังไม่ได้เขียนส่วนนี้ [1]</p>
<p><strong>และนี่เป็นเรื่องที่ผมอยากให้คุณรู้ก่อนใช้</strong> โมเดลนี้ไม่มีการระบุข้อจำกัดที่ชัดเจน เช่น ไม่รู้ว่าทำงานได้แค่ไหนกับเสียงใต้ หรือเสียงที่มีเสียงรบกวน หรือภาษาถิ่น [1]</p>
<h2>
<p>  เริ่มทดลองได้อย่างไร<br />
</p></h2>
<p><strong>ผมแนะนำให้ลองแบบนี้ครับ</strong> เลือกไฟล์เสียงของคุณเอง 1 ไฟล์ที่ <strong>มีสำเนียงหรือศัพท์เฉพาะที่คุณสนใจ</strong> แล้วลองถอดด้วยรุ่น <strong>small</strong> ก่อน</p>
<p>ถ้าคะแนนพอใช้ ก็ไม่ต้องใช้รุ่นใหญ่ <strong>แต่ถ้าไม่พอ การขยับไป medium หรือ large-v3 ก็ใช้โค้ดชุดเดิม</strong> แก้แค่ชื่อโมเดลในบรรทัดเดียว [1]</p>
<p><strong>และเพราะเป็น Apache 2.0 คุณลองในบริบทงานจริงได้เลยโดยไม่ต้องกังวลเรื่องสิทธิ์</strong> [1]</p>
<h3>
<p>  ข้อควรระวัง<br />
</p></h3>
<p><strong>หนึ่ง</strong> <strong>ผมไม่ได้รันโมเดลนี้</strong> ทั้งหมดเป็นการรายงานจากหน้าโมเดลบน Hugging Face [1][2]</p>
<p><strong>สอง</strong> <strong>ตัวเลข WER ทั้งหมดเป็นของทีมเอง</strong> บนชุดทดสอบ <code>common_voice_13_0</code> และใช้ Deepcut tokenizer <strong>ห้ามนำไปเทียบกับ WER ของทีมอื่นโดยตรง</strong> เพราะวิธีตัดคำต่างกัน [1]</p>
<p><strong>สาม</strong> <strong>ทีมไม่ได้ระบุข้อจำกัดการใช้งาน (limitations) ไว้</strong> หน้าโมเดลเขียนว่า &#8220;ต้องการข้อมูลเพิ่มเติม&#8221; ทั้งสองหัวข้อ [1]</p>
<p><strong>สี่</strong> <strong>ผมไม่พบวันที่เผยแพร่ที่ชัดเจนของรุ่น V4</strong> สิ่งที่ยืนยันได้คือ BibTeX ระบุปี 2022 สำหรับเวอร์ชันแรก [1]</p>
<p><strong>ห้า</strong> <strong>ตัวเลข downloads เปลี่ยนทุกวัน</strong> ตัวเลขที่ผมเขียนเป็นค่าที่ดึงจาก API ณ วันที่เขียน [2]</p>
<p><strong>หก</strong> <strong>มีโมเดลที่บุคคลอื่นสร้างต่อจากชุดนี้</strong> (เช่นรุ่นสำหรับงานดูแลผู้สูงอายุ หรือรุ่นแปลงสำหรับ MLX) ซึ่งผมไม่ได้ตรวจคุณภาพ [2]</p>
<p><strong>เจ็ด</strong> ผมทำงานบนระบบที่ใช้โมเดล AI และเขียนบทความนี้ด้วยความช่วยเหลือของ AI [5]</p>
<h3>
<p>  บทความที่เกี่ยวข้อง<br />
</p></h3>
<p><strong>นี่คือตอนที่ 2 ของซีรีส์</strong> ตอนที่ 1 พูดถึง <a href="https://dev.to/sarantoon">JaiTTS โคลนเสียงไทยที่คะแนนแม่นกว่ามนุษย์</a> ซึ่งเป็นด้าน &#8220;สร้างเสียง&#8221; ตอนนี้เป็นด้าน &#8220;ถอดเสียง&#8221; และตอนที่ 3 จะเป็น <strong>OpenJAI-v1.0-14B</strong> โมเดลภาษาจากทีมเดียวกันกับ JaiTTS</p>
<h3>
<p>  แหล่งอ้างอิง<br />
</p></h3>
<p>[1] Boribalburephan, A., Aung, Z. H., Pipatsrisawat, K., Achakulvisut, T., &#8220;Thonburian Whisper: A fine-tuned Whisper model for Thai automatic speech recognition&#8221; (2022), <a href="https://huggingface.co/biodatlab/whisper-th-medium-combined" rel="noopener noreferrer">https://huggingface.co/biodatlab/whisper-th-medium-combined</a></p>
<p>[2] &#8220;biodatlab&#8221; โมเดลทั้งหมดของทีม Biomedical and Data Lab, Hugging Face (เข้าถึง ก.ย. 2026), <a href="https://huggingface.co/biodatlab" rel="noopener noreferrer">https://huggingface.co/biodatlab</a></p>
<p>[3] &#8220;whisper-th-large-v3-combined&#8221; รุ่น Large V3 พร้อมค่าที่วัดได้, Hugging Face (เข้าถึง ก.ย. 2026), <a href="https://huggingface.co/biodatlab/whisper-th-large-v3-combined" rel="noopener noreferrer">https://huggingface.co/biodatlab/whisper-th-large-v3-combined</a></p>
<p>[4] &#8220;distill-whisper-th-large-v3&#8221; รุ่นกลั่นสำหรับผู้ที่ต้องการความเร็ว, Hugging Face (เข้าถึง ก.ย. 2026), <a href="https://huggingface.co/biodatlab/distill-whisper-th-large-v3" rel="noopener noreferrer">https://huggingface.co/biodatlab/distill-whisper-th-large-v3</a></p>
<p>[5] การเปิดเผยของผู้เขียน: บทความนี้เขียนโดยใช้ AI</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/thonburian-whisper-%e0%b8%96%e0%b8%ad%e0%b8%94%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99%e0%b9%80%e0%b8%84%e0%b8%a3%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%87.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bridging Temporal Machine Sagas and Flowable Human Workflows in BIAN Architectures</title>
		<link>https://codango.com/bridging-temporal-machine-sagas-and-flowable-human-workflows-in-bian-architectures.htm</link>
					<comments>https://codango.com/bridging-temporal-machine-sagas-and-flowable-human-workflows-in-bian-architectures.htm#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 01:43:17 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/bridging-temporal-machine-sagas-and-flowable-human-workflows-in-bian-architectures.htm</guid>

					<description><![CDATA[Modern core banking platform design presents a structural dilemma: high-frequency distributed systems demand sub-millisecond API execution, eventual consistency, and resilient retries, while corporate governance demands weeks-long human reviews, multi-tiered approvals, <a class="more-link" href="https://codango.com/bridging-temporal-machine-sagas-and-flowable-human-workflows-in-bian-architectures.htm">Continue reading <span class="screen-reader-text">  Bridging Temporal Machine Sagas and Flowable Human Workflows in BIAN Architectures</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Modern core banking platform design presents a structural dilemma: high-frequency distributed systems demand sub-millisecond API execution, eventual consistency, and resilient retries, while corporate governance demands weeks-long human reviews, multi-tiered approvals, and regulatory auditability.</p>
<p>Attempting to resolve both execution profiles with a single orchestration platform inevitably degrades system stability. Forcing BPMN (Business Process Model and Notation) engines to execute low-latency microservice Sagas leads to database state bloat and worker thread starvation. Conversely, using code-first workflow engines to manage multi-week human tasks obscures business visibility, hardcodes organizational approval chains, and compromises audit compliance.</p>
<p>Under the <strong>Xenon Architecture Standards</strong>, modern banking platform design resolves this tension through a dual-orchestration pattern mapped directly to Banking Industry Architecture Network (BIAN) service domains. This paper evaluates the operational boundaries, data-flow integrations, and failure patterns of pairing <strong>Temporal</strong> for machine-level distributed transaction Sagas with <strong>Flowable</strong> for human-in-the-loop business process management.</p>
<h3>
<p>  Architectural Mapping across BIAN Service Domains<br />
</p></h3>
<p>The Banking Industry Architecture Network (BIAN) defines standard Service Domains with strict encapsulation boundaries. Each Service Domain exposes functional capabilities through control keys and service operations. However, execution profiles vary drastically across domains.</p>
<p>By establishing an orchestration taxonomy, we assign orchestration responsibility based on state duration, execution throughput, and the presence of human intervention.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>BIAN Service Domain</th>
<th>Core Capability</th>
<th>Dominant Orchestrator</th>
<th>Primary Pattern</th>
<th>SLA / Latency Target</th>
<th>Consistency Model</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Payment Execution</strong></td>
<td>Automated clearing, ISO20022 message routing</td>
<td><strong>Temporal</strong></td>
<td>Saga (Compensating)</td>
<td>&lt; 200ms</td>
<td>Eventual (Deterministic)</td>
</tr>
<tr>
<td><strong>Position Keeping</strong></td>
<td>Ledger debit/credit updates</td>
<td><strong>Temporal</strong></td>
<td>Two-Phase Atomic Activity</td>
<td>&lt; 50ms</td>
<td>Strong Consistency</td>
</tr>
<tr>
<td><strong>Consumer Loan Origination</strong></td>
<td>End-to-end customer loan application lifecycle</td>
<td><strong>Flowable</strong></td>
<td>BPMN 2.0 User Task</td>
<td>Days to Weeks</td>
<td>Workflow State Persisted</td>
</tr>
<tr>
<td><strong>Credit Assessment</strong></td>
<td>Automated credit scoring &amp; manual underwriting</td>
<td><strong>Dual (Hybrid)</strong></td>
<td>Flowable drives; Temporal executes</td>
<td>Seconds (Auto) / Hours (Manual)</td>
<td>Mixed</td>
</tr>
<tr>
<td><strong>Customer Onboarding</strong></td>
<td>KYC verification, sanctions screening, account setup</td>
<td><strong>Dual (Hybrid)</strong></td>
<td>Flowable orchestrates stage gates; Temporal executes checks</td>
<td>Minutes to Days</td>
<td>Eventual</td>
</tr>
</tbody>
</table>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>                                   +------------------------------------------+
                                   |         Flowable BPMN Engine             |
                                   |  (Human Workflows &amp; SLA Management)      |
                                   +--------------------+---------------------+
                                                        |
                                            Async gRPC / Event Bus
                                                        |
                                   +--------------------+---------------------+
                                   |         Temporal Engine                  |
                                   |   (Machine Sagas &amp; API Resiliency)       |
                                   +----+---------------+----------------+----+
                                        |               |                |
                                        v               v                v
                               +----------------+ +-----------+ +-----------------+
                               | Payment Engine | | Ledger API| | Screening Engine|
                               +----------------+ +-----------+ +-----------------+

</code></pre>
</div>
<h2>
<p>  Deep Dive 1: Temporal for Machine-Level Sagas<br />
</p></h2>
<p>Temporal provides a developer-centric, code-first durable execution model. It persists the complete stack execution trace of an application, rendering code execution resilient to process crashes, network partitions, and downstream infrastructure outages.</p>
<p>Within the Xenon Architecture framework, Temporal handles machine-to-machine integration patterns where execution must be deterministic, programmatic, and sub-second.</p>
<h3>
<p>  The Saga Pattern in Core Ledger Operations<br />
</p></h3>
<p>When executing complex financial movements across isolated microservices (e.g., reserving balance in <em>Position Keeping</em>, checking limits in <em>Risk Management</em>, and posting to <em>General Ledger</em>), distributed ACID transactions are non-viable due to lock contention. Temporal enforces the Saga Pattern by registering explicit compensating actions for every forward transaction step.</p>
<p>Below is an enterprise Go implementation demonstrating a deterministic, fault-tolerant BIAN Payment Saga using Temporal SDK:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight go"><code><span class="k">package</span> <span class="n">sagas</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"fmt"</span>
    <span class="s">"time"</span>

    <span class="s">"go.temporal.io/sdk/workflow"</span>
<span class="p">)</span>

<span class="c">// PaymentSagaInput holds payload for BIAN Payment Execution</span>
<span class="k">type</span> <span class="n">PaymentSagaInput</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">PaymentID</span>        <span class="kt">string</span>
    <span class="n">SourceAccount</span>    <span class="kt">string</span>
    <span class="n">TargetAccount</span>    <span class="kt">string</span>
    <span class="n">Amount</span>           <span class="kt">float64</span>
    <span class="n">Currency</span>         <span class="kt">string</span>
    <span class="n">CorrelationID</span>    <span class="kt">string</span>
<span class="p">}</span>

<span class="c">// PaymentSagaWorkflow orchestrates machine-level execution with strict compensations</span>
<span class="k">func</span> <span class="n">PaymentSagaWorkflow</span><span class="p">(</span><span class="n">ctx</span> <span class="n">workflow</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">input</span> <span class="n">PaymentSagaInput</span><span class="p">)</span> <span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">options</span> <span class="o">:=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ActivityOptions</span><span class="p">{</span>
        <span class="n">StartToCloseTimeout</span><span class="o">:</span> <span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">,</span>
        <span class="n">RetryPolicy</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">workflow</span><span class="o">.</span><span class="n">RetryPolicy</span><span class="p">{</span>
            <span class="n">InitialInterval</span><span class="o">:</span>    <span class="m">100</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">,</span>
            <span class="n">BackoffCoefficient</span><span class="o">:</span> <span class="m">2.0</span><span class="p">,</span>
            <span class="n">MaximumAttempts</span><span class="o">:</span>    <span class="m">5</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">}</span>
    <span class="n">ctx</span> <span class="o">=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">WithActivityOptions</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>

    <span class="k">var</span> <span class="n">compensations</span> <span class="p">[]</span><span class="k">func</span><span class="p">(</span><span class="n">workflow</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="kt">error</span>
    <span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="c">// Execute compensations in reverse order on failure</span>
            <span class="n">compCtx</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">NewDisconnectedContext</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">compensations</span><span class="p">)</span> <span class="o">-</span> <span class="m">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span><span class="o">--</span> <span class="p">{</span>
                <span class="k">if</span> <span class="n">compErr</span> <span class="o">:=</span> <span class="n">compensations</span><span class="p">[</span><span class="n">i</span><span class="p">](</span><span class="n">compCtx</span><span class="p">);</span> <span class="n">compErr</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                    <span class="n">workflow</span><span class="o">.</span><span class="n">GetLogger</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"Compensation failed"</span><span class="p">,</span> <span class="s">"error"</span><span class="p">,</span> <span class="n">compErr</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}()</span>

    <span class="c">// Step 1: Reserve Funds in Source Account</span>
    <span class="k">var</span> <span class="n">reservationID</span> <span class="kt">string</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ExecuteActivity</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">ReserveFundsActivity</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">SourceAccount</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">Amount</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">CorrelationID</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">reservationID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to reserve funds: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="c">// Register Compensation</span>
    <span class="n">compensations</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">compensations</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">cCtx</span> <span class="n">workflow</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ExecuteActivity</span><span class="p">(</span><span class="n">cCtx</span><span class="p">,</span> <span class="n">CancelReservationActivity</span><span class="p">,</span> <span class="n">reservationID</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">CorrelationID</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">cCtx</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
    <span class="p">})</span>

    <span class="c">// Step 2: Perform Real-Time Sanctions Screening</span>
    <span class="k">var</span> <span class="n">passedScreening</span> <span class="kt">bool</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ExecuteActivity</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">ScreenTransactionActivity</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">PaymentID</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">Amount</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">passedScreening</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="o">!</span><span class="n">passedScreening</span> <span class="p">{</span>
        <span class="n">err</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"sanctions check rejected transaction"</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">err</span>
    <span class="p">}</span>

    <span class="c">// Step 3: Credit Target Account</span>
    <span class="k">var</span> <span class="n">postingID</span> <span class="kt">string</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ExecuteActivity</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">CreditAccountActivity</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">TargetAccount</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">Amount</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">CorrelationID</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">postingID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to credit target account: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="c">// Step 4: Finalize Reservation (Commit)</span>
    <span class="n">err</span> <span class="o">=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">ExecuteActivity</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">FinalizeReservationActivity</span><span class="p">,</span> <span class="n">reservationID</span><span class="p">,</span> <span class="n">postingID</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to finalize balance movement: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

</code></pre>
</div>
<h3>
<p>  Key Technical Advantages of Temporal in Financial Sagas<br />
</p></h3>
<ul>
<li>
<strong>Zero Polling Cost:</strong> Event-driven architecture suspends workers while waiting for external system responses without occupying active threads.</li>
<li>
<strong>Transparent Retries:</strong> Non-deterministic external failures (e.g., API timeouts) trigger exponential backoffs without polluting business domain state.</li>
<li>
<strong>Exact Execution Replay:</strong> System history event sourcing ensures state can be reconstructed precisely during platform disaster recovery events.</li>
</ul>
<h2>
<p>  Deep Dive 2: Flowable for Human-in-the-Loop Workflows<br />
</p></h2>
<p>Flowable implements BPMN 2.0 and CMMN (Case Management Model and Notation) standards. It excels where state persistence spans long periods, processes must adapt dynamically to human input, and compliance requires a clear visual domain map.</p>
<h3>
<p>  Human Approvals in Credit Assessment<br />
</p></h3>
<p>In BIAN Credit Assessment, automated scoring engines process standard requests instantaneously. However, applications flagged for risk exceptions must transition to manual underwriting.</p>
<p>Flowable models these long-lived process steps natively using User Tasks, Candidate Groups, and Escalation Timers.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight xml"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="nt">&lt;definitions</span> <span class="na">xmlns=</span><span class="s">"http://www.omg.org/spec/BPMN/20100524/MODEL"</span>
             <span class="na">xmlns:flowable=</span><span class="s">"http://flowable.org/bpmn"</span>
             <span class="na">targetNamespace=</span><span class="s">"BIAN/CreditAssessment"</span><span class="nt">&gt;</span>

  <span class="nt">&lt;process</span> <span class="na">id=</span><span class="s">"loanUnderwritingProcess"</span> <span class="na">name=</span><span class="s">"Loan Underwriting Process"</span> <span class="na">isExecutable=</span><span class="s">"true"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;startEvent</span> <span class="na">id=</span><span class="s">"startEvent"</span> <span class="na">name=</span><span class="s">"Loan Application Received"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"startEvent"</span> <span class="na">targetRef=</span><span class="s">"callAutomatedScoringSaga"</span> <span class="nt">/&gt;</span>

    <span class="c">&lt;!-- Java Delegate acting as a client bridge to Temporal Saga --&gt;</span>
    <span class="nt">&lt;serviceTask</span> <span class="na">id=</span><span class="s">"callAutomatedScoringSaga"</span> 
                 <span class="na">name=</span><span class="s">"Execute Machine Scoring Saga"</span> 
                 <span class="na">flowable:class=</span><span class="s">"com.xenon.banking.bridge.TemporalSagaDelegate"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"callAutomatedScoringSaga"</span> <span class="na">targetRef=</span><span class="s">"checkScoringDecision"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;exclusiveGateway</span> <span class="na">id=</span><span class="s">"checkScoringDecision"</span> <span class="na">name=</span><span class="s">"Approval Decision?"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"checkScoringDecision"</span> <span class="na">targetRef=</span><span class="s">"autoApproveEnd"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;conditionExpression</span> <span class="na">xsi:type=</span><span class="s">"tFormalExpression"</span><span class="nt">&gt;</span>${scoreOutcome == 'AUTO_APPROVED'}<span class="nt">&lt;/conditionExpression&gt;</span>
    <span class="nt">&lt;/sequenceFlow&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"checkScoringDecision"</span> <span class="na">targetRef=</span><span class="s">"manualUnderwritingTask"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;conditionExpression</span> <span class="na">xsi:type=</span><span class="s">"tFormalExpression"</span><span class="nt">&gt;</span>${scoreOutcome == 'REFER_TO_HUMAN'}<span class="nt">&lt;/conditionExpression&gt;</span>
    <span class="nt">&lt;/sequenceFlow&gt;</span>

    <span class="c">&lt;!-- Human Task Assignment --&gt;</span>
    <span class="nt">&lt;userTask</span> <span class="na">id=</span><span class="s">"manualUnderwritingTask"</span> 
              <span class="na">name=</span><span class="s">"Manual Credit Review"</span> 
              <span class="na">flowable:candidateGroups=</span><span class="s">"underwriters"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;documentation&gt;</span>
        Underwriter review required for Application ID: ${applicationId}. Credit Score border case.
      <span class="nt">&lt;/documentation&gt;</span>
    <span class="nt">&lt;/userTask&gt;</span>

    <span class="c">&lt;!-- Boundary Timer Event for SLA Escalation --&gt;</span>
    <span class="nt">&lt;boundaryEvent</span> <span class="na">id=</span><span class="s">"slaTimer"</span> <span class="na">attachedToRef=</span><span class="s">"manualUnderwritingTask"</span> <span class="na">cancelActivity=</span><span class="s">"false"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;timerEventDefinition&gt;</span>
        <span class="nt">&lt;timeDuration&gt;</span>PT24H<span class="nt">&lt;/timeDuration&gt;</span>
      <span class="nt">&lt;/timerEventDefinition&gt;</span>
    <span class="nt">&lt;/boundaryEvent&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"slaTimer"</span> <span class="na">targetRef=</span><span class="s">"escalateToManager"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;userTask</span> <span class="na">id=</span><span class="s">"escalateToManager"</span> 
              <span class="na">name=</span><span class="s">"Manager Override Review"</span> 
              <span class="na">flowable:candidateGroups=</span><span class="s">"credit_managers"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"manualUnderwritingTask"</span> <span class="na">targetRef=</span><span class="s">"manualDecisionGateway"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;exclusiveGateway</span> <span class="na">id=</span><span class="s">"manualDecisionGateway"</span> <span class="na">name=</span><span class="s">"Approved?"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"manualDecisionGateway"</span> <span class="na">targetRef=</span><span class="s">"approvedEnd"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;conditionExpression</span> <span class="na">xsi:type=</span><span class="s">"tFormalExpression"</span><span class="nt">&gt;</span>${underwriterDecision == 'APPROVED'}<span class="nt">&lt;/conditionExpression&gt;</span>
    <span class="nt">&lt;/sequenceFlow&gt;</span>
    <span class="nt">&lt;sequenceFlow</span> <span class="na">sourceRef=</span><span class="s">"manualDecisionGateway"</span> <span class="na">targetRef=</span><span class="s">"rejectedEnd"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;conditionExpression</span> <span class="na">xsi:type=</span><span class="s">"tFormalExpression"</span><span class="nt">&gt;</span>${underwriterDecision == 'REJECTED'}<span class="nt">&lt;/conditionExpression&gt;</span>
    <span class="nt">&lt;/sequenceFlow&gt;</span>

    <span class="nt">&lt;endEvent</span> <span class="na">id=</span><span class="s">"autoApproveEnd"</span> <span class="na">name=</span><span class="s">"Auto Approved"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;endEvent</span> <span class="na">id=</span><span class="s">"approvedEnd"</span> <span class="na">name=</span><span class="s">"Manually Approved"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;endEvent</span> <span class="na">id=</span><span class="s">"rejectedEnd"</span> <span class="na">name=</span><span class="s">"Application Rejected"</span> <span class="nt">/&gt;</span>

  <span class="nt">&lt;/process&gt;</span>
<span class="nt">&lt;/definitions&gt;</span>

</code></pre>
</div>
<h3>
<p>  Key Technical Advantages of Flowable for Business Operations<br />
</p></h3>
<ul>
<li>
<strong>Declarative BPMN/CMMN Visualizations:</strong> Enables risk, compliance, and auditing teams to inspect and validate business pathways directly using standardised graphical representations.</li>
<li>
<strong>Organizational Hierarchy Awareness:</strong> Dynamic resolution of user roles, delegations, managerial escalations, and regional assignment groups.</li>
<li>
<strong>Operational Task APIs:</strong> Native capabilities for listing, claiming, reassigning, and completing tasks through enterprise web application interfaces.</li>
</ul>
<h2>
<p>  The Integration Architecture: Bridging the Dual-Orchestration Gap<br />
</p></h2>
<p>The architectural key to Xenon&#8217;s framework lies in preventing direct, tight coupling between Flowable and Temporal. Flowable must remain agnostic to microservice execution mechanics, and Temporal must not manage long-lived human process state.</p>
<p>We achieve integration via <strong>Event-Driven Asynchronous Signals</strong> using Apache Kafka or gRPC Bridge Delegates.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>+-----------------------------------------------------------------------------------+
| FLOWABLE PROCESS ENGINE                                                           |
|                                                                                   |
|  [BPMN: Start] ---&gt; [ServiceTask: Trigger Saga] ---&gt; [Receive Task: Wait Signal]  |
+-----------------------------------|--------------------------^--------------------+
                                    |                          |
                         1. Async gRPC Execution        4. Signal Execution Callback
                                    |                          |
+-----------------------------------|--------------------------|--------------------+
| INTEGRATION LAYER                 v                          |                    |
|                         +-------------------+      +------------------+           |
|                         | Temporal Client   |      | Signal REST/gRPC |           |
|                         | Dispatcher        |      | Client           |           |
|                         +---------+---------+      +---------^--------+           |
+-----------------------------------|--------------------------|--------------------+
                                    |                          |
                         2. Start Workflow              3. Complete Saga
                                    |                          |
+-----------------------------------|--------------------------|--------------------+
| TEMPORAL ENGINE                   v                          |                    |
|                                                              |                    |
|    [Start Saga Workflow] ---&gt; [Execute Microservices] -------+                    |
+-----------------------------------------------------------------------------------+

</code></pre>
</div>
<h3>
<p>  The Java Bridge Implementation<br />
</p></h3>
<p>When Flowable encounters a step that requires machine execution (e.g., executing a complex Temporal credit-scoring Saga), a custom Java Delegate triggers the Temporal workflow asynchronously. The Flowable process then enters a native <code>ReceiveTask</code> state, awaiting a system signal.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight java"><code><span class="kn">package</span> <span class="nn">com.xenon.banking.bridge</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">io.temporal.client.WorkflowClient</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">io.temporal.client.WorkflowOptions</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.flowable.engine.delegate.DelegateExecution</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.flowable.engine.delegate.JavaDelegate</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Component</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.xenon.banking.temporal.CreditScoringWorkflow</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.xenon.banking.temporal.model.ScoringRequest</span><span class="o">;</span>

<span class="nd">@Component</span><span class="o">(</span><span class="s">"temporalSagaDelegate"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TemporalSagaDelegate</span> <span class="kd">implements</span> <span class="nc">JavaDelegate</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">WorkflowClient</span> <span class="n">temporalWorkflowClient</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TemporalSagaDelegate</span><span class="o">(</span><span class="nc">WorkflowClient</span> <span class="n">temporalWorkflowClient</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">temporalWorkflowClient</span> <span class="o">=</span> <span class="n">temporalWorkflowClient</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">execute</span><span class="o">(</span><span class="nc">DelegateExecution</span> <span class="n">execution</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">applicationId</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">execution</span><span class="o">.</span><span class="na">getVariable</span><span class="o">(</span><span class="s">"applicationId"</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">executionId</span> <span class="o">=</span> <span class="n">execution</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>

        <span class="c1">// 1. Construct non-blocking options with correlation key</span>
        <span class="nc">WorkflowOptions</span> <span class="n">options</span> <span class="o">=</span> <span class="nc">WorkflowOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setTaskQueue</span><span class="o">(</span><span class="s">"CREDIT_SCORING_TASK_QUEUE"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setWorkflowId</span><span class="o">(</span><span class="s">"CreditScore-"</span> <span class="o">+</span> <span class="n">applicationId</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="c1">// 2. Instantiate Stub</span>
        <span class="nc">CreditScoringWorkflow</span> <span class="n">workflowStub</span> <span class="o">=</span> <span class="n">temporalWorkflowClient</span><span class="o">.</span><span class="na">newWorkflowStub</span><span class="o">(</span><span class="nc">CreditScoringWorkflow</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">options</span><span class="o">);</span>

        <span class="c1">// 3. Prepare Input with Flowable Execution Callback Reference</span>
        <span class="nc">ScoringRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ScoringRequest</span><span class="o">();</span>
        <span class="n">request</span><span class="o">.</span><span class="na">setApplicationId</span><span class="o">(</span><span class="n">applicationId</span><span class="o">);</span>
        <span class="n">request</span><span class="o">.</span><span class="na">setFlowableExecutionId</span><span class="o">(</span><span class="n">executionId</span><span class="o">);</span>

        <span class="c1">// 4. Trigger Temporal Saga Asynchronously</span>
        <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="nl">workflowStub:</span><span class="o">:</span><span class="n">executeScoring</span><span class="o">,</span> <span class="n">request</span><span class="o">);</span>

        <span class="c1">// 5. Flowable context moves to a ReceiveTask node immediately after this execution</span>
    <span class="o">}</span>
<span class="o">}</span>

</code></pre>
</div>
<h3>
<p>  Completion Callback: Temporal signaling back to Flowable<br />
</p></h3>
<p>Upon completion of the Saga, a Temporal Activity posts an asynchronous completion signal back to Flowable&#8217;s Runtime Service:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight java"><code><span class="kn">package</span> <span class="nn">com.xenon.banking.bridge</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.flowable.engine.RuntimeService</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Service</span><span class="o">;</span>

<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FlowableCallbackService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RuntimeService</span> <span class="n">flowableRuntimeService</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">FlowableCallbackService</span><span class="o">(</span><span class="nc">RuntimeService</span> <span class="n">flowableRuntimeService</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">flowableRuntimeService</span> <span class="o">=</span> <span class="n">flowableRuntimeService</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleSagaCompletion</span><span class="o">(</span><span class="nc">String</span> <span class="n">flowableExecutionId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">outcome</span><span class="o">,</span> <span class="kt">double</span> <span class="n">score</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Pass result parameters back to Flowable engine memory context</span>
        <span class="n">flowableRuntimeService</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="n">flowableExecutionId</span><span class="o">,</span> <span class="s">"scoreOutcome"</span><span class="o">,</span> <span class="n">outcome</span><span class="o">);</span>
        <span class="n">flowableRuntimeService</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="n">flowableExecutionId</span><span class="o">,</span> <span class="s">"creditScore"</span><span class="o">,</span> <span class="n">score</span><span class="o">);</span>

        <span class="c1">// Trigger Flowable step advancement out of ReceiveTask</span>
        <span class="n">flowableRuntimeService</span><span class="o">.</span><span class="na">trigger</span><span class="o">(</span><span class="n">flowableExecutionId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

</code></pre>
</div>
<h2>
<p>  Failure Modes, State Recovery, and Transactional Boundaries<br />
</p></h2>
<p>In a dual-orchestration topology, edge cases occur primarily at the operational boundary between the two systems. System design must accommodate asymmetric execution states.</p>
<h3>
<p>  Scenario 1: Temporal Saga Fails Completely<br />
</p></h3>
<p>If a machine Saga fails and all compensating actions complete successfully, the machine state remains consistent, but the business transaction cannot proceed automatically.</p>
<ul>
<li>
<strong>Mitigation:</strong> The Temporal completion activity signals Flowable with an <code>outcome = 'SYSTEM_ERROR'</code> payload.</li>
<li>
<strong>Flowable Behavior:</strong> Flowable evaluates this outcome using a BPMN Exclusive Gateway and routes the application to an operational exception handling queue (Human User Task) for operational staff to investigate or manual retry.</li>
</ul>
<h3>
<p>  Scenario 2: Human Task Times Out or Is Cancelled<br />
</p></h3>
<p>A customer cancels a loan request while the human underwriting task is pending in Flowable, or the review period exceeds defined SLA boundaries.</p>
<ul>
<li>
<strong>Mitigation:</strong> Flowable triggers an Interrupting Boundary Event (Timer or Signal).</li>
<li>
<strong>Temporal Cancellation:</strong> Flowable dispatches an explicit RPC request to the Temporal Client API (<code>workflowStub.cancel()</code>). Temporal intercepts the cancellation request, runs any required cleanup activities, and releases allocated system resources (such as active fund holds).</li>
</ul>
<h3>
<p>  State Synchronization Patterns<br />
</p></h3>
<p>To maintain audit integrity, state synchronization across both systems must conform to standard operational principles:</p>
<ol>
<li>
<strong>Correlation Keys:</strong> Every transaction must carry a global BIAN Correlation ID (<code>correlationId = "BIAN-ORIG-2026-98234"</code>). This identifier must be passed across Flowable executions, Temporal Workflows, Kafka Headers, and downstream microservice trace contexts.</li>
<li>
<strong>Idempotent Signal Ingestion:</strong> Signals sent from Temporal to Flowable must use deterministic execution references (<code>executionId</code>). If network instability causes duplicate delivery of a completion signal, Flowable&#8217;s execution check rejects processing of redundant payloads.</li>
</ol>
<h2>
<p>  Observability, Distributed Tracing, and Auditability<br />
</p></h2>
<p>Operating dual orchestrators requires unified observability across both machine traces and human audit logs.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>[Customer Application] 
       │
       ▼  W3C TraceContext (traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01)
┌──────────────┐
│  Flowable    │ (Generates Human Audit Record: UserTask 'Approved' by User #4092)
└──────┬───────┘
       │ OpenTelemetry Context Propagation (gRPC / Kafka Headers)
       ▼
┌──────────────┐
│  Temporal    │ (Generates Microservice Execution Span: Activity 'DebitLedger' 4ms)
└──────────────┘

</code></pre>
</div>
<ul>
<li>
<strong>OpenTelemetry Propagation:</strong> Inject the W3C <code>traceparent</code> header into Flowable process variables. When calling Temporal through the gRPC bridge client, populate the OpenTelemetry context. This enables end-to-end tracing in tools like Jaeger or Datadog, spanning from human UI interaction down to database commits.</li>
<li><strong>Audit Trail Bifurcation:</strong></li>
<li>
<strong>Regulatory Audit (Compliance):</strong> Sourced from Flowable&#8217;s historical DB tables (<code>ACT_HI_*</code>), providing clear verification of <em>which human approved what action at what time</em>.</li>
<li>
<strong>System Operations Audit (IT):</strong> Sourced from Temporal&#8217;s Event History store, verifying <em>which microservice APIs executed, retry counts, payload hashes, and exact execution timing</em>.</li>
</ul>
<p>Adopting a dual-orchestration pattern based on Temporal and Flowable provides a structured design for BIAN-compliant banking platforms. By delegating machine-level transactional consistency to Temporal&#8217;s Saga implementation and long-running organizational processes to Flowable&#8217;s BPMN engine, core banking systems achieve sub-second technical performance alongside resilient human workflows.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/bridging-temporal-machine-sagas-and-flowable-human-workflows-in-bian-architectures.htm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
