<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.clauneck.workers.dev</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.clauneck.workers.dev/feed"/>
    <language>en</language>
    <item>
      <title>Why We Built an Android SMS Gateway API (And the Pivot That Forced Us to Build It)</title>
      <dc:creator>Raffy</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:35:54 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/raffy_d/why-we-built-an-android-sms-gateway-api-and-the-pivot-that-forced-us-to-build-it-1l4k</link>
      <guid>https://dev.clauneck.workers.dev/raffy_d/why-we-built-an-android-sms-gateway-api-and-the-pivot-that-forced-us-to-build-it-1l4k</guid>
      <description>&lt;p&gt;For a long time, I did what every ambitious engineer is told to do. I climbed the corporate ladder. I made it to the height of my career, managed engineering teams, and dealt with the heavy stress that comes with enterprise infrastructure.&lt;/p&gt;

&lt;p&gt;But eventually, corporate inertia and office politics catch up with you. After a complex, challenging corporate exit, I found myself at a major crossroads. It was a stressful time that tested my spirit, but it ultimately brought me down to a place of radical humility.&lt;/p&gt;

&lt;p&gt;In that space, I realized I wanted to build things with a greater purpose. When everything else felt uncertain, getting back to writing code became my sanctuary. It is where I find my peace. I teamed up with some incredible colleagues, and together, we started building a new suite of software under a shared vision. We wanted to help small businesses get access to enterprise-grade tools without the enterprise price tag.&lt;/p&gt;

&lt;p&gt;Our roadmap is to build full SaaS applications for small-and-medium enterprises, especially around autonomous AI customer service. But as we began mapping out the features, we hit a massive roadblock that forced us to build the foundation first.&lt;/p&gt;

&lt;p&gt;That foundational project is Relayion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Problem with Modern Developer SMS
&lt;/h2&gt;

&lt;p&gt;We were looking into how local businesses handle customer communication. Everyone talks about connecting apps to Facebook Messenger or WhatsApp, but for local businesses like a neighborhood medical clinic or an automotive shop, traditional SMS is still the absolute lifeblood. They need it for automated appointment reminders, quick customer inquiries, and sending secure one-time passwords.&lt;/p&gt;

&lt;p&gt;The problem is that if a developer wants to build a system that handles autonomous, two-way customer service over SMS, using traditional cloud providers is a trap.&lt;/p&gt;

&lt;p&gt;The costs scale instantly because they charge heavy fees for every single outbound and inbound message. On top of that, you face a bureaucratic nightmare of carrier regulations and endless registration forms just to get approved to send a basic automated text.&lt;/p&gt;

&lt;p&gt;The ironic part is that businesses are already actively using their physical phones with local SIM cards and unlimited text plans to talk to their clients and run ads. They already own the hardware and pay for the service. We just needed a way to bridge that existing hardware to the web systems that can actually automate, streamline, and scale those processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach to the Architecture
&lt;/h2&gt;

&lt;p&gt;We decided to build an infrastructure that lets any web server, app, or AI agent send and receive texts natively through a physical Android device. We did not want to build an overnight, fly-by-night tool. Our engineering team spent months refining this mechanism to make sure it handles messages with extreme care and reliability.&lt;/p&gt;

&lt;p&gt;Our system is engineered for instant responsiveness. The moment your server hits our endpoint, the message routes to the phone safely. We built a robust cloud buffer in front of the hardware. If your phone temporarily loses cellular signal or the network drops out for a minute, our cloud holds onto the messages securely and flushes them to the device the exact millisecond it is stable. You do not get dropped messages or lost data, which is exactly how a reliable engine should work.&lt;/p&gt;

&lt;p&gt;On the flip side, when a customer replies to that text, the phone captures the incoming SMS and instantly relays a webhook back to your system or AI model for a seamless, two-way loop.&lt;/p&gt;

&lt;p&gt;We wanted to make this incredibly lightweight for developers to deploy. Once our Android application is running on your device, sending a programmatic text from Node.js, Python, or a no-code workflow tool like n8n looks exactly like a standard three-line API curl request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building in Public and Moving Forward
&lt;/h2&gt;

&lt;p&gt;Relayion isn't perfect, and as engineers, we know software is an evolving canvas. But our team strives every day to bring it closer to perfection.&lt;/p&gt;

&lt;p&gt;This journey has taught me radical humility. I have fully accepted that I am exactly where God wants me to be, and I have surrendered the steering wheel to Him. Continuing down this path, sharing our engineering milestones, and building software that genuinely helps people is where our focus is.&lt;/p&gt;

&lt;p&gt;We built Relayion to give indie hackers, small business owners, and developers the kind of affordable, robust infrastructure they usually get priced out of. Whether you are building an AI chatbot, automating a local clinic, or just messing around with a weekend project, we would love for you to try it out.&lt;/p&gt;

&lt;p&gt;You can check out our platform and setup guides at relayion.com. Drop a comment below if you have ever tried building an SMS gateway or ran into these carrier pricing walls. Our team would love to swap notes with you.&lt;/p&gt;

</description>
      <category>career</category>
      <category>learning</category>
      <category>showdev</category>
      <category>management</category>
    </item>
    <item>
      <title>I Built an AI App in 30 Minutes.</title>
      <dc:creator>Irvan Gerhana Septiyana</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:35:45 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/uigerhana/i-built-an-ai-app-in-30-minutes-2bmk</link>
      <guid>https://dev.clauneck.workers.dev/uigerhana/i-built-an-ai-app-in-30-minutes-2bmk</guid>
      <description>&lt;h2&gt;
  
  
  It Started Falling Apart Three Days Later.
&lt;/h2&gt;

&lt;p&gt;Over the past year, AI coding tools have completely changed how I write software.&lt;/p&gt;

&lt;p&gt;Cursor.&lt;/p&gt;

&lt;p&gt;Claude Code.&lt;/p&gt;

&lt;p&gt;GitHub Copilot.&lt;/p&gt;

&lt;p&gt;OpenAI Codex.&lt;/p&gt;

&lt;p&gt;They've become part of my daily workflow.&lt;/p&gt;

&lt;p&gt;I can scaffold APIs in minutes.&lt;/p&gt;

&lt;p&gt;Generate React components almost instantly.&lt;/p&gt;

&lt;p&gt;Write documentation faster than ever before.&lt;/p&gt;

&lt;p&gt;It's honestly impressive.&lt;/p&gt;

&lt;p&gt;A few months ago, I decided to see how far I could push it.&lt;/p&gt;

&lt;p&gt;I challenged myself to build an AI-powered application as quickly as possible.&lt;/p&gt;

&lt;p&gt;Within about thirty minutes, I had something that actually worked.&lt;/p&gt;

&lt;p&gt;The UI looked clean.&lt;/p&gt;

&lt;p&gt;The API responded correctly.&lt;/p&gt;

&lt;p&gt;The demo was convincing.&lt;/p&gt;

&lt;p&gt;I was excited.&lt;/p&gt;

&lt;p&gt;Three days later...&lt;/p&gt;

&lt;p&gt;I found myself rewriting most of it.&lt;/p&gt;

&lt;p&gt;Not because the AI generated bad code.&lt;/p&gt;

&lt;p&gt;Because I skipped software engineering.&lt;/p&gt;




&lt;h1&gt;
  
  
  AI Didn't Cause The Problem
&lt;/h1&gt;

&lt;p&gt;It's tempting to blame the tool.&lt;/p&gt;

&lt;p&gt;But the tool wasn't the issue.&lt;/p&gt;

&lt;p&gt;The code was mostly fine.&lt;/p&gt;

&lt;p&gt;The real problem was that I optimized for one metric:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't spend enough time thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data models&lt;/li&gt;
&lt;li&gt;business rules&lt;/li&gt;
&lt;li&gt;architecture&lt;/li&gt;
&lt;li&gt;service boundaries&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;li&gt;long-term maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application worked.&lt;/p&gt;

&lt;p&gt;The system didn't.&lt;/p&gt;

&lt;p&gt;There's a huge difference.&lt;/p&gt;




&lt;h1&gt;
  
  
  Building Features Isn't Building Systems
&lt;/h1&gt;

&lt;p&gt;Modern AI is incredibly good at producing code.&lt;/p&gt;

&lt;p&gt;Ask it to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;CRUD endpoints&lt;/li&gt;
&lt;li&gt;React dashboards&lt;/li&gt;
&lt;li&gt;FastAPI routes&lt;/li&gt;
&lt;li&gt;SQL queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and you'll probably get something useful.&lt;/p&gt;

&lt;p&gt;But production software isn't simply a collection of features.&lt;/p&gt;

&lt;p&gt;It's a collection of decisions.&lt;/p&gt;

&lt;p&gt;Questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where should business logic live?&lt;/li&gt;
&lt;li&gt;Which service owns this data?&lt;/li&gt;
&lt;li&gt;How do we version our APIs?&lt;/li&gt;
&lt;li&gt;What happens when downstream services fail?&lt;/li&gt;
&lt;li&gt;Which component becomes the source of truth?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those aren't code-generation problems.&lt;/p&gt;

&lt;p&gt;They're engineering problems.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Architecture Debt Nobody Talks About
&lt;/h1&gt;

&lt;p&gt;We often talk about technical debt.&lt;/p&gt;

&lt;p&gt;Lately I've started thinking about another kind of debt.&lt;/p&gt;

&lt;p&gt;Architecture debt.&lt;/p&gt;

&lt;p&gt;It happens when software grows faster than understanding.&lt;/p&gt;

&lt;p&gt;Every AI-generated feature introduces another assumption.&lt;/p&gt;

&lt;p&gt;Another dependency.&lt;/p&gt;

&lt;p&gt;Another shortcut.&lt;/p&gt;

&lt;p&gt;Another duplicated business rule.&lt;/p&gt;

&lt;p&gt;Everything still works...&lt;/p&gt;

&lt;p&gt;Until it doesn't.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Real Example
&lt;/h1&gt;

&lt;p&gt;Recently I worked on a Transaction Intelligence System for enterprise financial automation.&lt;/p&gt;

&lt;p&gt;At first glance, the project looked like another NLP pipeline.&lt;/p&gt;

&lt;p&gt;Take a bank statement.&lt;/p&gt;

&lt;p&gt;Extract entities.&lt;/p&gt;

&lt;p&gt;Return JSON.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Except it wasn't.&lt;/p&gt;

&lt;p&gt;Before I could train a model, I had to design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a canonical data model&lt;/li&gt;
&lt;li&gt;business taxonomies&lt;/li&gt;
&lt;li&gt;synthetic datasets&lt;/li&gt;
&lt;li&gt;entity relationships&lt;/li&gt;
&lt;li&gt;reconciliation rules&lt;/li&gt;
&lt;li&gt;evaluation pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ironically...&lt;/p&gt;

&lt;p&gt;The AI model turned out to be one of the easier parts.&lt;/p&gt;

&lt;p&gt;The difficult part was understanding the business.&lt;/p&gt;




&lt;h1&gt;
  
  
  AI Can Generate Code.
&lt;/h1&gt;

&lt;h2&gt;
  
  
  It Can't Invent Your Business.
&lt;/h2&gt;

&lt;p&gt;Imagine asking an AI assistant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Has invoice MFG-INV-000157 already been paid?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model can't answer that question unless someone has already built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a customer master&lt;/li&gt;
&lt;li&gt;an invoice master&lt;/li&gt;
&lt;li&gt;contract relationships&lt;/li&gt;
&lt;li&gt;payment history&lt;/li&gt;
&lt;li&gt;reconciliation logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The language model doesn't magically know how your business operates.&lt;/p&gt;

&lt;p&gt;Someone has to teach the system.&lt;/p&gt;

&lt;p&gt;That "someone" is still the engineer.&lt;/p&gt;




&lt;h1&gt;
  
  
  From Vibe Coding to Engineering
&lt;/h1&gt;

&lt;p&gt;I love how quickly AI lets me prototype ideas.&lt;/p&gt;

&lt;p&gt;I wouldn't want to go back.&lt;/p&gt;

&lt;p&gt;But I've also changed how I work.&lt;/p&gt;

&lt;p&gt;Instead of asking AI:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build this feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I now ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Help me design this system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of generating endpoints first...&lt;/p&gt;

&lt;p&gt;I design the architecture.&lt;/p&gt;

&lt;p&gt;Instead of creating tables...&lt;/p&gt;

&lt;p&gt;I model the business.&lt;/p&gt;

&lt;p&gt;Instead of optimizing prompts...&lt;/p&gt;

&lt;p&gt;I optimize understanding.&lt;/p&gt;

&lt;p&gt;The code becomes much easier afterwards.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Most Valuable Skill Isn't Coding Anymore
&lt;/h1&gt;

&lt;p&gt;For years we measured engineers by how much code they wrote.&lt;/p&gt;

&lt;p&gt;Today AI writes a significant portion of that code.&lt;/p&gt;

&lt;p&gt;So what becomes valuable?&lt;/p&gt;

&lt;p&gt;Understanding.&lt;/p&gt;

&lt;p&gt;Architecture.&lt;/p&gt;

&lt;p&gt;Decision making.&lt;/p&gt;

&lt;p&gt;Data modeling.&lt;/p&gt;

&lt;p&gt;Communication.&lt;/p&gt;

&lt;p&gt;Business context.&lt;/p&gt;

&lt;p&gt;Those skills cannot simply be autocomplete-generated.&lt;/p&gt;

&lt;p&gt;At least not today.&lt;/p&gt;




&lt;h1&gt;
  
  
  My Biggest Lesson
&lt;/h1&gt;

&lt;p&gt;The biggest lesson wasn't that AI is overhyped.&lt;/p&gt;

&lt;p&gt;It wasn't that AI can't code.&lt;/p&gt;

&lt;p&gt;It absolutely can.&lt;/p&gt;

&lt;p&gt;The lesson was much simpler.&lt;/p&gt;

&lt;p&gt;AI accelerates implementation.&lt;/p&gt;

&lt;p&gt;It doesn't replace engineering.&lt;/p&gt;

&lt;p&gt;And the larger the system becomes...&lt;/p&gt;

&lt;p&gt;The more valuable engineering becomes.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The future isn't about writing code faster.&lt;/p&gt;

&lt;p&gt;We're already doing that.&lt;/p&gt;

&lt;p&gt;The future is about designing systems that continue working six months after the demo.&lt;/p&gt;

&lt;p&gt;Because shipping software is easy.&lt;/p&gt;

&lt;p&gt;Maintaining software is difficult.&lt;/p&gt;

&lt;p&gt;AI helps us write code.&lt;/p&gt;

&lt;p&gt;Software engineering helps us build products.&lt;/p&gt;

&lt;p&gt;And those two things are not the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;Over the last several months, I've been documenting how I built a complete &lt;strong&gt;Enterprise AI Transaction Intelligence System&lt;/strong&gt; from scratch.&lt;/p&gt;

&lt;p&gt;Instead of focusing on prompts or AI demos, the project covers the engineering foundations behind production-ready AI systems, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical Data Architecture&lt;/li&gt;
&lt;li&gt;Synthetic Enterprise Dataset Design&lt;/li&gt;
&lt;li&gt;Financial Named Entity Recognition (NER)&lt;/li&gt;
&lt;li&gt;Entity Resolution&lt;/li&gt;
&lt;li&gt;Business Rules &amp;amp; Automated Reconciliation&lt;/li&gt;
&lt;li&gt;FastAPI Production API&lt;/li&gt;
&lt;li&gt;End-to-End Evaluation &amp;amp; Benchmarking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in building AI systems that solve real business problems—not just impressive demos—you can explore the complete implementation here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Enterprise AI Automation Blueprint&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://uigerhana.gumroad.com/l/enterprise-ai-automation-blueprint" rel="noopener noreferrer"&gt;https://uigerhana.gumroad.com/l/enterprise-ai-automation-blueprint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bundle includes three in-depth handbooks, production-ready Python source code, synthetic datasets, architecture documentation, and practical examples based on a real enterprise use case.&lt;/p&gt;

&lt;p&gt;If you found this article useful, I'd love to connect.&lt;/p&gt;

&lt;p&gt;I'm currently publishing a series on Enterprise AI Engineering, AI Automation, Software Architecture, and Production AI Systems here on Dev.to.&lt;/p&gt;

&lt;p&gt;Happy building. 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Custom Autonomous Drone Stack - Part 3: The Zero-Velocity Hover</title>
      <dc:creator>Harsh Pandhe</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:33:00 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/harshpandhe/building-a-custom-autonomous-drone-stack-part-3-the-zero-velocity-hover-46n7</link>
      <guid>https://dev.clauneck.workers.dev/harshpandhe/building-a-custom-autonomous-drone-stack-part-3-the-zero-velocity-hover-46n7</guid>
      <description>&lt;h2&gt;
  
  
  Surviving GPS-Denied Environments with &lt;code&gt;VELOCITY_MASK&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you're flying a micro-UAV indoors using only an optical flow sensor and a downward-facing LiDAR, standard autonomous flight commands will quickly disappoint you.&lt;/p&gt;

&lt;p&gt;When you issue a traditional &lt;code&gt;MAV_CMD_NAV_TAKEOFF&lt;/code&gt;, ArduPilot expects to have access to global position information. Without GPS, the navigation stack may refuse to move.&lt;/p&gt;

&lt;p&gt;To work around this, we bypassed the higher-level navigation controller and injected raw MAVLink commands over serial using &lt;code&gt;set_attitude_target_send()&lt;/code&gt;. This allowed the drone to climb using only its onboard sensors.&lt;/p&gt;

&lt;p&gt;Getting airborne, however, is only half the battle.&lt;/p&gt;

&lt;p&gt;Keeping the drone in the air is where things get interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Zero-Throttle Drop" Trap
&lt;/h2&gt;

&lt;p&gt;During our first tethered test flights, we managed to climb to roughly one meter in &lt;code&gt;GUIDED&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;To command a stable hover, we switched the vehicle into &lt;code&gt;LOITER&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The instant the mode changed, the motors slowed down and the drone dropped.&lt;/p&gt;

&lt;p&gt;What happened?&lt;/p&gt;

&lt;p&gt;The issue wasn't hardware.&lt;/p&gt;

&lt;p&gt;It was a software conflict.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GUIDED&lt;/code&gt; mode is completely autonomous and ignores pilot stick inputs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LOITER&lt;/code&gt;, on the other hand, is a hybrid mode.&lt;/p&gt;

&lt;p&gt;As soon as the flight controller entered &lt;code&gt;LOITER&lt;/code&gt;, it checked the RC transmitter.&lt;/p&gt;

&lt;p&gt;Our throttle stick was resting at zero.&lt;/p&gt;

&lt;p&gt;ArduPilot interpreted that as a pilot command to descend.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pure Autonomy Fix: &lt;code&gt;VELOCITY_MASK&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of relying on physical joysticks, we stayed entirely inside &lt;code&gt;GUIDED&lt;/code&gt; mode and continuously sent a zero-velocity command.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;set_position_target_local_ned_send()&lt;/code&gt;, we can specify a bitmask that tells the Pixhawk exactly which fields matter.&lt;/p&gt;

&lt;p&gt;A mask value of &lt;code&gt;3527&lt;/code&gt; (&lt;code&gt;0b110111000111&lt;/code&gt;) means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignore position coordinates&lt;/li&gt;
&lt;li&gt;Ignore acceleration&lt;/li&gt;
&lt;li&gt;Ignore yaw&lt;/li&gt;
&lt;li&gt;Enforce X, Y, and Z velocity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By commanding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vx = 0
Vy = 0
Vz = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we are effectively telling the flight controller:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hold perfectly still.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Velocity Hold Loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;VELOCITY_MASK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3527&lt;/span&gt;

&lt;span class="c1"&gt;# Maintain hover for 10 seconds
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_position_target_local_ned_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mavutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mavlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAV_FRAME_LOCAL_NED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;VELOCITY_MASK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Position (ignored)
&lt;/span&gt;        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Velocity = 0 m/s
&lt;/span&gt;        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Acceleration (ignored)
&lt;/span&gt;        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;         &lt;span class="c1"&gt;# Yaw and yaw rate (ignored)
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this loop running, optical flow measurements continuously feed the EKF3.&lt;/p&gt;

&lt;p&gt;If the drone drifts due to airflow or frame vibrations, the Pixhawk automatically adjusts motor outputs to force the physical velocity back toward zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Velocity Commands Work Better Than Position Commands Indoors
&lt;/h2&gt;

&lt;p&gt;Position commands assume absolute position knowledge.&lt;/p&gt;

&lt;p&gt;Indoors, position estimates often drift.&lt;/p&gt;

&lt;p&gt;Velocity control is far more forgiving.&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be exactly at coordinate X=0.52 meters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Maintain zero velocity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This lets the flight controller use optical flow and LiDAR to make tiny corrections without fighting an imperfect global reference frame.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Panic Button
&lt;/h2&gt;

&lt;p&gt;Never fly without an emergency failsafe.&lt;/p&gt;

&lt;p&gt;If your Python script crashes unexpectedly while the vehicle remains armed, the drone will continue executing the last command it received.&lt;/p&gt;

&lt;p&gt;That's a bad day.&lt;/p&gt;

&lt;p&gt;We wrapped the entire flight sequence inside an emergency interceptor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Emergency Landing Function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;emergency_land&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[!!!] EMERGENCY: INITIATING LAND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mode_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode_mapping&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LAND&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_mode_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mavutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mavlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAV_MODE_FLAG_CUSTOM_MODE_ENABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mode_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command_long_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mavutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mavlink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAV_CMD_NAV_LAND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Wrapping the Flight Loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Arm
&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. Takeoff
&lt;/span&gt;
    &lt;span class="c1"&gt;# 3. Velocity hold
&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nf"&gt;emergency_land&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;emergency_land&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, pressing &lt;code&gt;Ctrl+C&lt;/code&gt; immediately overrides the current mission and forces a controlled descent.&lt;/p&gt;

&lt;p&gt;The flight controller handles the landing sequence and disarms automatically after touchdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;One of the biggest lessons we learned was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate high-level logic from low-level reflexes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let your companion computer make decisions.&lt;/p&gt;

&lt;p&gt;Let the flight controller do what it does best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sensor fusion&lt;/li&gt;
&lt;li&gt;State estimation&lt;/li&gt;
&lt;li&gt;Motor control&lt;/li&gt;
&lt;li&gt;Stabilization&lt;/li&gt;
&lt;li&gt;Recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By respecting this separation, you can build an autonomous stack that remains reliable even in GPS-denied environments.&lt;/p&gt;

&lt;p&gt;And that's exactly what robotics is all about.&lt;/p&gt;

&lt;p&gt;Not replacing the flight controller.&lt;/p&gt;

&lt;p&gt;Working with it.&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>drones</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>Augmented Team Quality: The Attrition Problem</title>
      <dc:creator>Dennis Vorobyov</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:26:54 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/d_v_/augmented-team-quality-the-attrition-problem-2n22</link>
      <guid>https://dev.clauneck.workers.dev/d_v_/augmented-team-quality-the-attrition-problem-2n22</guid>
      <description>&lt;p&gt;Everest Group's 2024 Staff Augmentation report found that 48% of augmented teams experience "high attrition" — defined as annual engineer turnover exceeding 25%. For a 5-person augmented team, that means losing 1-2 engineers per year. Each departure triggers the same costs as internal turnover — &lt;a href="https://eltexsoft.com/blog/vendor-lock-in-knowledge-loss/" rel="noopener noreferrer"&gt;knowledge loss&lt;/a&gt;, ramp-up time, team disruption — but the client has no control over the staffing decisions because the engineers are employed by the vendor.&lt;/p&gt;

&lt;p&gt;I run a &lt;a href="https://eltexsoft.com/staffing/team-augmentation/" rel="noopener noreferrer"&gt;staff augmentation&lt;/a&gt; business. The 48% number is the industry average, not our number. Our average engagement is 3+ years with the same engineers. The difference is the model, not the market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Augmented Teams Churn
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Utilization-driven rotation
&lt;/h3&gt;

&lt;p&gt;Large staffing firms optimize for utilization, not client satisfaction. When a higher-paying engagement opens, the vendor moves the engineer from your project to the new one and assigns a replacement. From the vendor's perspective, this is rational — the same engineer generates more revenue on the new project. From your perspective, you just lost 6 months of domain knowledge and got someone who needs 2-3 months to ramp up.&lt;/p&gt;

&lt;p&gt;The contractual language usually permits this. "The vendor reserves the right to substitute equivalent resources with reasonable notice." "Reasonable notice" might be 2 weeks. "Equivalent resources" might mean "same title, different person." The equivalency is in resume keywords, not in production experience with your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engineer dissatisfaction with body-shop model
&lt;/h3&gt;

&lt;p&gt;Good engineers do not want to be rotated between projects every 6 months. They want to build something, see it grow, and take pride in the result. The body-shop model — where the engineer is a fungible resource assigned wherever revenue is highest — treats engineers as commodities. The best engineers leave the body shop for companies that treat them as people. What remains is the engineers who could not get a better position.&lt;/p&gt;

&lt;p&gt;This creates a negative selection spiral: the vendor rotates out the good engineers (either voluntarily or because the engineer quits), replaces them with weaker engineers, and the client's project quality declines. The client complains about quality. The vendor promises to "upgrade the team." The upgrade is another engineer who will be rotated out in 6 months.&lt;/p&gt;

&lt;h3&gt;
  
  
  No investment in engineer growth
&lt;/h3&gt;

&lt;p&gt;Staffing firms that rotate engineers every 6 months have no incentive to invest in their technical growth. Training costs money. An engineer who gets trained and then leaves for a competitor is a loss. So the firm does not train. The engineers stagnate. They leave for firms that invest in their growth. The attrition cycle continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 48% Attrition Costs the Client
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Knowledge transfer on repeat
&lt;/h3&gt;

&lt;p&gt;Each departing engineer takes undocumented context with them. The new engineer spends 2-3 months ramping up. During ramp-up, they operate at 50% productivity and consume senior team members' time through questions and pairing. For a 5-person team losing 1-2 engineers per year, you are permanently in ramp-up mode. At any given time, 20-40% of the team is below full productivity.&lt;/p&gt;

&lt;p&gt;The annualized cost: 2 engineers × 3 months ramp-up × 50% productivity loss × $50-100/hour = $48,000-$96,000 in reduced output per year. That is before counting the senior engineer time consumed by onboarding the replacements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Team cohesion destruction
&lt;/h3&gt;

&lt;p&gt;Software engineering is a team sport. Engineers who have worked together for years develop shared understanding: coding conventions, architectural patterns, debugging instincts, and communication shortcuts. A team with 48% annual turnover never develops this cohesion. It is permanently a group of individuals, not a team.&lt;/p&gt;

&lt;p&gt;The performance gap between a cohesive team and a collection of individuals is well-documented. Google's Project Aristotle found that team psychological safety — built through stable relationships — is the #1 predictor of team effectiveness. You cannot build psychological safety when half the team changes every year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vendor management overhead
&lt;/h3&gt;

&lt;p&gt;Each rotation triggers: vendor communication about the change, review of the replacement's resume, interview with the replacement, transition planning, knowledge transfer sessions, and 2-3 months of closer supervision until the new engineer is up to speed. For the client's product manager or engineering lead, each rotation consumes 20-40 hours of management time.&lt;/p&gt;

&lt;p&gt;At 2 rotations per year on a 5-person team, that is 40-80 hours/year of vendor management overhead driven entirely by attrition. At $80-$150/hour for the client's internal manager, that is $3,200-$12,000/year in management cost — not counting the opportunity cost of what that manager would have done with those hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Our Attrition Is Different
&lt;/h2&gt;

&lt;p&gt;Our model is not utilization-driven. We do not rotate engineers to higher-paying projects because our engagements are structured as long-term partnerships, not resource placements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We do not optimize for utilization.&lt;/strong&gt; When an engineer is assigned to &lt;a href="https://eltexsoft.com/cases/heytutor/" rel="noopener noreferrer"&gt;HeyTutor&lt;/a&gt;, they work on HeyTutor. For 9 years, in this case. They are not pulled to a new project because a new client offered $10/hour more. Our revenue model is based on stable retainers, not maximizing hourly rate per engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We invest in engineer growth.&lt;/strong&gt; Our engineers who built &lt;a href="https://eltexsoft.com/tech/laravel/" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt; applications 5 years ago now build &lt;a href="https://eltexsoft.com/services/ai-development/" rel="noopener noreferrer"&gt;AI products&lt;/a&gt;. The engineers who built Nautical Commerce's &lt;a href="https://eltexsoft.com/tech/django/" rel="noopener noreferrer"&gt;Django&lt;/a&gt; marketplace now work on healthcare platforms. The technical growth keeps the work interesting. Interesting work retains engineers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We assign engineers to domains they care about.&lt;/strong&gt; An engineer who is passionate about &lt;a href="https://eltexsoft.com/industries/fintech/" rel="noopener noreferrer"&gt;FinTech&lt;/a&gt; works on FinTech projects. An engineer who loves &lt;a href="https://eltexsoft.com/services/mobile-development/" rel="noopener noreferrer"&gt;mobile development&lt;/a&gt; builds mobile apps. Matching interest to assignment is not something utilization-optimized firms can do — they assign whoever is available. We assign whoever is right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We treat engineers as the product, not as the commodity.&lt;/strong&gt; Our clients stay for 3+ years because the engineers are excellent. If we rotated them, the clients would leave. Our business model depends on retention at both ends: engineer retention and client retention are the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Evaluate Augmentation Partners
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ask about tenure
&lt;/h3&gt;

&lt;p&gt;"What is the average tenure of your engineers on client projects?" If the answer is "6-12 months" or "it varies," that is 48% attrition territory. If the answer is "our average engagement is 3+ years," verify it with references.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask about rotation policy
&lt;/h3&gt;

&lt;p&gt;"Under what circumstances would you substitute an engineer on my project?" The right answer: "Only if the engineer leaves the company or you request a change." The wrong answer: "We reserve the right to substitute equivalent resources." That is the utilization-driven rotation clause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask for named engineers before signing
&lt;/h3&gt;

&lt;p&gt;"Who specifically will work on my project?" If the vendor cannot name the engineers before the contract, the team will be assembled from the available bench after signing. You do not know who you are getting. The &lt;a href="https://eltexsoft.com/blog/outsourcing-failure-skill-mismatch/" rel="noopener noreferrer"&gt;bait-and-switch&lt;/a&gt; risk is high.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check client retention
&lt;/h3&gt;

&lt;p&gt;Vendors who retain engineers retain clients. Ask: "What percentage of your clients have been with you for 2+ years?" A firm with high engineer attrition also has high client attrition. The two are directly correlated.&lt;/p&gt;

&lt;p&gt;Our numbers: &lt;a href="https://eltexsoft.com/cases/heytutor/" rel="noopener noreferrer"&gt;HeyTutor&lt;/a&gt; (9 years), &lt;a href="https://eltexsoft.com/cases/myflyright/" rel="noopener noreferrer"&gt;MyFlyRight&lt;/a&gt; (10 years), &lt;a href="https://eltexsoft.com/cases/greekhouse/" rel="noopener noreferrer"&gt;Greek House&lt;/a&gt; (4 years), &lt;a href="https://eltexsoft.com/cases/snapwire/" rel="noopener noreferrer"&gt;Snapwire&lt;/a&gt; (2.5 years), &lt;a href="https://eltexsoft.com/cases/ripe/" rel="noopener noreferrer"&gt;Ripe&lt;/a&gt; (5 years). Those are not cherry-picked. Those are our major engagements. The pattern is the proof.&lt;/p&gt;

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

&lt;p&gt;A 5-person augmented team at $50/hour with 48% annual attrition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct cost: 5 × $50 × 160 × 12 = $480,000&lt;/li&gt;
&lt;li&gt;Attrition cost (knowledge loss, ramp-up, management): ~$96,000-$144,000/year&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effective cost: $576,000-$624,000 (20-30% above invoice)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 5-person augmented team at $70/hour with &amp;lt;10% annual attrition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct cost: 5 × $70 × 160 × 12 = $672,000&lt;/li&gt;
&lt;li&gt;Attrition cost: ~$10,000-$20,000/year (rare single rotations)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effective cost: $682,000-$692,000&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is $58,000-$68,000 — about 10%. But the stable team ships more, breaks less, and requires less management. The total value delivered per dollar is higher with the stable team despite the higher hourly rate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eltexsoft.com/staffing/team-augmentation/" rel="noopener noreferrer"&gt;$50-99/hour&lt;/a&gt; with team stability is a better deal than $40/hour with 48% annual churn. The invoice is higher. The outcome is better. The total cost is comparable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eltexsoft.com/contact/" rel="noopener noreferrer"&gt;Talk to us →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last updated April 14, 2024&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.clauneck.workers.dev/blog/security-alert-false-positives/"&gt;Older&lt;br&gt;
 62% of Security Teams Say 25%+ of Alerts Are False Positives&lt;/a&gt;   &lt;a href="https://dev.clauneck.workers.dev/blog/hidden-outsourcing-costs/"&gt;Newer&lt;br&gt;
  The True Cost of Outsourcing Is 20% More Than the Quote&lt;/a&gt;&lt;/p&gt;

</description>
      <category>management</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Zero-Dependency Favicon Generation with Canvas API — Build Your Own in 50 Lines</title>
      <dc:creator>swift king</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:24:45 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/jamiepark-design/zero-dependency-favicon-generation-with-canvas-api-build-your-own-in-50-lines-2bjj</link>
      <guid>https://dev.clauneck.workers.dev/jamiepark-design/zero-dependency-favicon-generation-with-canvas-api-build-your-own-in-50-lines-2bjj</guid>
      <description>&lt;p&gt;Every React project I've worked on eventually adds a favicon generation dependency. &lt;code&gt;favicons&lt;/code&gt; alone pulls in &lt;code&gt;sharp&lt;/code&gt;, which pulls in native binaries, which breaks on that one teammate's M1 Mac and eats 15 minutes of your afternoon. I got tired of it and built a zero-dependency favicon generator using nothing but the Canvas API. It ships all six sizes you need for a modern web app in under 50 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Actually Need in 2026
&lt;/h2&gt;

&lt;p&gt;The minimum favicon set has grown. Here's what covers every major browser and platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;favicon.ico&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32×32&lt;/td&gt;
&lt;td&gt;Legacy browser fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;favicon-16.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16×16&lt;/td&gt;
&lt;td&gt;Browser tab (small screens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;favicon-32.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32×32&lt;/td&gt;
&lt;td&gt;Browser tab (standard)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apple-touch-icon.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;180×180&lt;/td&gt;
&lt;td&gt;iOS Home Screen, Safari&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;icon-192.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;192×192&lt;/td&gt;
&lt;td&gt;Android PWA, Chrome install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;icon-512.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×512&lt;/td&gt;
&lt;td&gt;PWA splash screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's six files. Most generators create 15+ — including manifest.json, browserconfig.xml, and sizes no one uses anymore. You don't need the bloat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Canvas API Approach
&lt;/h2&gt;

&lt;p&gt;The trick is that Canvas can render SVG paths (via &lt;code&gt;Path2D&lt;/code&gt;) and then export to PNG at any resolution. No DOM, no fonts, no external libraries.&lt;/p&gt;

&lt;p&gt;Here's the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateFaviconSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgPathData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e293b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;favicon-16.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;favicon-32.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apple-touch-icon.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;icon-192.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;icon-512.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Path2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgPathData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: Canvas's &lt;code&gt;Path2D&lt;/code&gt; constructor accepts the exact same path data string as SVG's &lt;code&gt;d&lt;/code&gt; attribute. You can prototype your icon in any SVG editor, copy the &lt;code&gt;d="..."&lt;/code&gt; value, and feed it directly into the function above. No parsing, no conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Use &lt;code&gt;toBlob()&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;toBlob()&lt;/code&gt; would be more memory-efficient than &lt;code&gt;toDataURL()&lt;/code&gt;, but it's asynchronous and creates event-handling complexity when generating multiple sizes. For favicon-sized images (max 512×512), &lt;code&gt;toDataURL()&lt;/code&gt; keeps the code synchronous and readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Click → ZIP of All Sizes
&lt;/h2&gt;

&lt;p&gt;Wrapping this into a proper tool means adding &lt;code&gt;JSZip&lt;/code&gt; for packaging and a file input for custom images. I built exactly that at &lt;a href="https://genfavicon.org" rel="noopener noreferrer"&gt;genfavicon.org&lt;/a&gt; — upload any image, get all six favicon sizes in one ZIP, plus the HTML &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags pre-generated. The entire thing runs in the browser; nothing gets uploaded to any server.&lt;/p&gt;

&lt;p&gt;But if you want to build your own pipeline — maybe you need a custom color palette or want to integrate it into a design system tool — the Canvas approach above is the foundation. No &lt;code&gt;sharp&lt;/code&gt;, no &lt;code&gt;imagemagick&lt;/code&gt;, no native deps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ICO Format: One Quirk
&lt;/h2&gt;

&lt;p&gt;Canvas can export PNG and JPEG, but not ICO. For &lt;code&gt;favicon.ico&lt;/code&gt;, you have two clean options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skip it.&lt;/strong&gt; Chrome, Firefox, Edge, and Safari all support PNG favicons via &lt;code&gt;&amp;lt;link rel="icon" type="image/png"&amp;gt;&lt;/code&gt;. The ICO format is only needed for IE11 and some ancient Android browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tiny encoder.&lt;/strong&gt; A minimal ICO encoder is about 30 lines — it's just a BMP header + PNG data wrapped in an ICO container.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Personally, I generate a 32×32 PNG and use &lt;code&gt;&amp;lt;link rel="icon" type="image/png" sizes="32x32"&amp;gt;&lt;/code&gt; for modern browsers plus a tiny &lt;code&gt;.ico&lt;/code&gt; fallback for the stragglers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win: Your Build Step Gets Simpler
&lt;/h2&gt;

&lt;p&gt;Every project I've switched from &lt;code&gt;sharp&lt;/code&gt;-based favicon generation to an in-browser tool has deleted roughly three dependencies and 20 lines of build config. For Next.js specifically, this means no &lt;code&gt;sharp&lt;/code&gt; in &lt;code&gt;node_modules&lt;/code&gt;, no native rebuilds on &lt;code&gt;npm install&lt;/code&gt;, and no "works on my machine" moments when the designer updates the icon.&lt;/p&gt;

&lt;p&gt;If you don't want to write the Canvas code yourself, &lt;a href="https://genfavicon.org" rel="noopener noreferrer"&gt;genfavicon.org&lt;/a&gt; does the same thing with a drag-and-drop UI. But the Canvas approach is genuinely simple enough that you can embed it in an internal tool in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code snippets in this article are MIT-licensed. Use them however you want.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>So Tired of Reading to Keep Up</title>
      <dc:creator>Rick Gonzalez</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:21:32 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/rickgonzalez/so-tired-of-reading-to-keep-up-ahm</link>
      <guid>https://dev.clauneck.workers.dev/rickgonzalez/so-tired-of-reading-to-keep-up-ahm</guid>
      <description>&lt;p&gt;Frontier models are very good, but we shouldn't hand them the keys to our digital world just yet. Lenzon.ai celebrates the creativity and ingenuity behind modern software by surfacing the remarkable architecture engineers have built over the last few decades. AI would be nowhere without those foundations, and the future of software depends on growing human understanding so we can keep shaping what comes next.&lt;/p&gt;

&lt;p&gt;This isn't man against machine. It's man with the machine, and our perspective has to scale alongside the systems we're building. So, to date, it's been read and read and read! &lt;/p&gt;

&lt;p&gt;I built Lenzon because my eyes were starting this weird little twitch. I wanted to make it simple to gain a shared view of complex logic, fast. AI agents do the heavy lifting on the analysis, but it's the succinct visual and audio explainers that actually move understanding from one head to another. That's the loop: AI accelerates the read, humans get smarter, humans keep steering.&lt;br&gt;
Lenzon is open source (Apache 2.0) and free for public repos. I'm very interested in what you think about using a technology like this for your PRs, so please share here. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here are some example pull request explainers that took on average six minutes to create.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lenzon.ai/viewer/cmqo9zi2z0000o9zdphhvuq3c?voice=google-chirp3" rel="noopener noreferrer"&gt;Lenzon Whole Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lenzon.ai/viewer/cmqk5h1sv0000gu1uxivao8hy?voice=google-chirp3" rel="noopener noreferrer"&gt;VS Code Public Pull Request&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.lenzon.ai/viewer/cmqpj09k20008dks4fakzztyn?voice=google-chirp3" rel="noopener noreferrer"&gt;Policy Flow PR with findings&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your AI Code Has 6 Secret Hits. Only 3 Ship in the npm Package.</title>
      <dc:creator>Alexey Spinov</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:20:03 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/alex_spinov/your-ai-code-has-6-secret-hits-only-3-ship-in-the-npm-package-39jf</link>
      <guid>https://dev.clauneck.workers.dev/alex_spinov/your-ai-code-has-6-secret-hits-only-3-ship-in-the-npm-package-39jf</guid>
      <description>&lt;p&gt;Secrets in a published npm package are a different set from secrets in your repo. A secret scanner reads the whole git tree; &lt;code&gt;npm pack&lt;/code&gt; ships only the &lt;code&gt;files&lt;/code&gt; allowlist in &lt;code&gt;package.json&lt;/code&gt;. &lt;code&gt;leak_probe.py&lt;/code&gt; measures both and prints the gap. On the fixture below it found 6 hits and flagged 3 as actually shipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scanner reads your git tree. The packager reads the &lt;code&gt;files&lt;/code&gt; allowlist. They are not the same file set.&lt;/li&gt;
&lt;li&gt;On the test package: 6 secret hits total, 3 of them ship in the tarball, 3 are git-only (a &lt;code&gt;test/&lt;/code&gt; fake and a root &lt;code&gt;run.log&lt;/code&gt;, both outside the &lt;code&gt;files&lt;/code&gt; allowlist). Exit 1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;leak_probe.py&lt;/code&gt; is ~80 lines of Python: provider regexes + entropy + a packaging filter. No network, no model, no exec, no install.&lt;/li&gt;
&lt;li&gt;A hit is a SIGNAL, not a confirmed live secret. Verify ship-status with &lt;code&gt;npm pack --dry-run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Runs in about 60 seconds, no API key. Code and fixtures are in the post.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The blind spot nobody scans for
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;gitleaks&lt;/code&gt; or &lt;code&gt;trufflehog&lt;/code&gt; and you get a list of secrets in your working tree. Useful. But that list answers a question about your repo, not about your release. The thing you push to npm is whatever &lt;code&gt;npm pack&lt;/code&gt; decides to include, and &lt;code&gt;npm pack&lt;/code&gt; has its own rules: the &lt;code&gt;files&lt;/code&gt; array is an allowlist, &lt;code&gt;.npmignore&lt;/code&gt; subtracts from whatever is left, and a handful of files (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;README.md&lt;/code&gt;) always ship.&lt;/p&gt;

&lt;p&gt;So two failure modes hide in the gap.&lt;/p&gt;

&lt;p&gt;One: a secret your scanner flagged loud and red sits in &lt;code&gt;test/fixtures.js&lt;/code&gt;, which is not in your &lt;code&gt;files&lt;/code&gt; allowlist, so it never ships. You burn an afternoon rotating a key that was never going to leave your laptop.&lt;/p&gt;

&lt;p&gt;Two, the one that hurts: a secret in &lt;code&gt;src/&lt;/code&gt; that your team triaged as "low priority, it's just a placeholder" ships in the public tarball to every install. The scanner saw it. The risk triage downranked it. The packager shipped it anyway.&lt;/p&gt;

&lt;p&gt;I have not pushed a leaked key to npm myself. But the shape of this is not theoretical. GitGuardian's State of Secrets Sprawl 2026 (published 17 March 2026) reports that &lt;strong&gt;Claude Code-assisted commits showed a 3.2% secret-leak rate versus a 1.5% baseline across all public GitHub commits&lt;/strong&gt;, and that &lt;strong&gt;AI-service secrets reached 1,275,105 in 2025, up 81% year over year&lt;/strong&gt; (&lt;a href="https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/" rel="noopener noreferrer"&gt;blog.gitguardian.com&lt;/a&gt;). Their headline number: 28.65 million new hardcoded secrets added to public GitHub in 2025. Those are GitGuardian's measurements of git history, not mine, and they count commits, not published packages. I am citing them for context, not as my result. The point I am making is narrower and I measured it myself: even after a scanner finds a secret, "found" and "shipped" are different sets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contrarian part, stated so you can break it
&lt;/h2&gt;

&lt;p&gt;Here is the claim, sharp enough to argue with: &lt;strong&gt;running a secret scanner on your repo does not tell you what ships.&lt;/strong&gt; A secret can be flagged by the scanner and never leave your machine. A secret the scanner downranks can ship to every install.&lt;/p&gt;

&lt;p&gt;That is falsifiable, and I want it to be. The ground truth is &lt;code&gt;npm pack --dry-run&lt;/code&gt;, which lists the exact files in the tarball. If that set always equaled your git tree, the claim would be false and &lt;code&gt;leak_probe.py&lt;/code&gt; would be pointless. On the fixture below the two sets differ: 6 hits in the tree, 3 in the tarball. Run &lt;code&gt;npm pack --dry-run&lt;/code&gt; on the same fixture and you will see &lt;code&gt;src/&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt; listed, &lt;code&gt;test/&lt;/code&gt; and &lt;code&gt;run.log&lt;/code&gt; absent. That is the whole argument in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool: ~80 lines, four rules
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;leak_probe.py&lt;/code&gt; does four deterministic things and nothing else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Provider regexes&lt;/strong&gt; for vendor-published key shapes: &lt;code&gt;AKIA…&lt;/code&gt; (AWS), &lt;code&gt;sk-…&lt;/code&gt; (OpenAI), &lt;code&gt;sk_live_…&lt;/code&gt; (Stripe), &lt;code&gt;ghp_…&lt;/code&gt; (GitHub PAT), &lt;code&gt;xox[baprs]-…&lt;/code&gt; (Slack).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic high-entropy assignment&lt;/strong&gt;: a &lt;code&gt;name = "long literal"&lt;/code&gt; where the literal has Shannon entropy at least 3.5 and is not pure letters. The entropy gate is there to drop &lt;code&gt;apiKey = "your_api_key_here"&lt;/code&gt; style placeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The packaging filter&lt;/strong&gt; (this is the part a plain scanner does not have): for each file, decide whether &lt;code&gt;npm pack&lt;/code&gt; ships it, using the &lt;code&gt;files&lt;/code&gt; allowlist, &lt;code&gt;.npmignore&lt;/code&gt;, and the always-shipped set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density&lt;/strong&gt;: hits per 100 scanned lines, a local number, not a market average.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Exit code is the gate: &lt;code&gt;1&lt;/code&gt; if anything that shipped contains a hit, &lt;code&gt;0&lt;/code&gt; if every hit is git-only or there are none, &lt;code&gt;2&lt;/code&gt; for a broken manifest or bad usage. Drop it in a pre-publish hook and a shipping secret fails the build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fnmatch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="n"&gt;PROVIDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIA[0-9A-Z]{16}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-[A-Za-z0-9]{20,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stripe_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_[0-9A-Za-z]{16,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github_pat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghp_[A-Za-z0-9]{36}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slack_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xox[baprs]-[0-9A-Za-z-]{10,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ASSIGN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(?ix)(secret|token|api[_-]?key|password|access[_-]?key)\s*[:=]\s*[&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;]([^&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;]{12,})[&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shannon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The packaging filter is the only clever bit, and it is short. The &lt;code&gt;files&lt;/code&gt; field is an allowlist: if it exists, a file ships only if it is named there. &lt;code&gt;.npmignore&lt;/code&gt; then subtracts. &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;README.md&lt;/code&gt; always ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;package.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;                      &lt;span class="c1"&gt;# npm always ships these
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                &lt;span class="c1"&gt;# `files` is an allowlist: opt-in only
&lt;/span&gt;        &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fnmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full script is in the draft repo for this post. It is one file, standard library only, Python 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it: the real output
&lt;/h2&gt;

&lt;p&gt;Three fixtures. A clean package, a leaky one, and a broken manifest. Here is the verbatim run on Python 3.13.5. Every key in these fixtures is either a published vendor placeholder (&lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt; is AWS's own) or a synthetic, non-functional value shaped to match a provider regex. None is a live secret.&lt;/p&gt;

&lt;p&gt;Clean package: secrets come from &lt;code&gt;process.env&lt;/code&gt;, &lt;code&gt;files: ["src"]&lt;/code&gt;, nothing hardcoded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 leak_probe.py fixtures/clean_pkg
scanned_lines=14  secret_hits=0  density_per_100=0.0  WILL_SHIP_in_package=0
[exit 0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero hits, exit 0. That is the falsifiable floor: a clean tree produces a clean result. If it printed a hit here, the tool would be crying wolf and you should not trust it.&lt;/p&gt;

&lt;p&gt;Now the leaky package. Three real-shaped keys in &lt;code&gt;src/secrets.js&lt;/code&gt; (ships, because &lt;code&gt;files: ["src", "dist"]&lt;/code&gt;), a fake key plus a weak password in &lt;code&gt;test/fixtures.js&lt;/code&gt; (does not ship, &lt;code&gt;test/&lt;/code&gt; is not in &lt;code&gt;files&lt;/code&gt;), and one key echoed into &lt;code&gt;run.log&lt;/code&gt; at the package root (does not ship, because a root &lt;code&gt;run.log&lt;/code&gt; is outside the &lt;code&gt;files&lt;/code&gt; allowlist; the &lt;code&gt;.npmignore&lt;/code&gt; rule &lt;code&gt;*.log&lt;/code&gt; is a redundant second belt if &lt;code&gt;files&lt;/code&gt; is ever removed).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 leak_probe.py fixtures/leaky_pkg
scanned_lines=23  secret_hits=6  density_per_100=26.087  WILL_SHIP_in_package=3
  SHIPS    aws_access_key  regex         AKIAIOS...  src/secrets.js
  SHIPS    github_pat      regex         ghp_aZ8...  src/secrets.js
  SHIPS    stripe_secret   regex         sk_live...  src/secrets.js
  git-only aws_access_key  regex         AKIAIOS...  run.log
  git-only openai_key      regex         sk-test...  test/fixtures.js
  git-only password        entropy&amp;gt;=3.5  superse...  test/fixtures.js
[exit 1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six hits. Three ship. Three git-only. A naive count says "6 secrets, panic." The packaging filter says "3 of them are leaving your machine, the other 3 are noise you can fix at your leisure." That difference is the whole reason the tool exists. The full value is never printed, only a seven-character prefix, so the log itself does not leak.&lt;/p&gt;

&lt;p&gt;Broken manifest, so you cannot reason about what ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 leak_probe.py fixtures/bad_pkg
error: package.json is not valid JSON
[exit 2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 2, message on stderr, nothing on stdout. Fail loud rather than guess the allowlist.&lt;/p&gt;

&lt;p&gt;It is deterministic. I hashed stdout twice for each fixture and the digests match, so this slots into CI without flakiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# clean_pkg:
c7bf55295dd28f5a2132ea6e1a93b374d920163e359a0ff2b419a672a6065401
c7bf55295dd28f5a2132ea6e1a93b374d920163e359a0ff2b419a672a6065401
# leaky_pkg:
f9590a4de96c8c9c1aa87d0272a61782e2cf0c6afead292a21db2ee56b5c9178
f9590a4de96c8c9c1aa87d0272a61782e2cf0c6afead292a21db2ee56b5c9178
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this is NOT
&lt;/h2&gt;

&lt;p&gt;I would rather you trust the boundaries than oversell the tool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A hit is a signal, not proof of a live secret.&lt;/strong&gt; Regex and entropy match shapes, not validity. &lt;code&gt;leak_probe.py&lt;/code&gt; does not call any provider to check if a key is real, active, or already revoked. That network call is exactly what keeps it offline and safe to run anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives are real.&lt;/strong&gt; Example keys in docs (&lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt; is AWS's own published placeholder), test fixtures, rotated keys, and committed-but-dead values all trip the regexes. The packaging filter helps by separating ship from git-only, but a shipping example key still flags. Keep an allowlist for known-safe values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False negatives are real too.&lt;/strong&gt; A secret built at runtime from &lt;code&gt;process.env&lt;/code&gt;, concatenated from parts, or injected after the scan runs will not appear as a literal. Build output produced after the scan is invisible. Non-standard key formats slip past the provider list. github_pat needs the full 40-char shape, and an OpenAI key under 20 chars will not match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The packaging filter is an approximation of &lt;code&gt;npm pack&lt;/code&gt;, not a reimplementation.&lt;/strong&gt; It models the common &lt;code&gt;files&lt;/code&gt; and &lt;code&gt;.npmignore&lt;/code&gt; semantics. It does not cover every npm edge case (nested ignore files, &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;files&lt;/code&gt; globs beyond the basics, hoisting quirks). It does not handle PyPI sdists or &lt;code&gt;MANIFEST.in&lt;/code&gt; at all; that is a direction, not a feature. The ground truth is &lt;code&gt;npm pack --dry-run&lt;/code&gt;. Treat this as a fast pre-filter, then verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is detection, not remediation.&lt;/strong&gt; It does not rotate, revoke, or prove validity. It tells you to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this differs from the neighbors
&lt;/h2&gt;

&lt;p&gt;If you have read the other tools in this series, two distinctions matter so you do not think this is a rerun.&lt;/p&gt;

&lt;p&gt;Measuring &lt;a href="https://finops.spinov.online/blog/blast-radius-ai-agent-api-key/" rel="noopener noreferrer"&gt;the blast radius of a leaked AI agent API key&lt;/a&gt; is about a key you already know is compromised: what can it touch, how far does the damage reach. That is a later stage. &lt;code&gt;leak_probe.py&lt;/code&gt; is upstream of that, at detection time, before anything is known to be compromised and before the package is even built. Both sit downstream of &lt;a href="https://finops.spinov.online/blog/pre-execution-gate-for-ai-agents/" rel="noopener noreferrer"&gt;a pre-execution gate for AI agents&lt;/a&gt;: the same instinct to stop a bad action before it runs, applied here to a bad publish before it ships.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://finops.spinov.online/blog/dependency-gap-auditor/" rel="noopener noreferrer"&gt;declared-vs-imported dependency gap auditor&lt;/a&gt; compares declared dependencies against imported ones. Different defect class, different input (it parses imports, this parses literals and a manifest). The shared theme is the one running through &lt;a href="https://finops.spinov.online/blog/your-agent-returns-200-and-lies/" rel="noopener noreferrer"&gt;an agent that returns 200 and lies&lt;/a&gt; and &lt;a href="https://finops.spinov.online/blog/green-checkmark-auditor/" rel="noopener noreferrer"&gt;auditing AI-generated tests behind a green checkmark&lt;/a&gt;: a green signal is not the same as a true one. Your scanner passing is not the same as your tarball being clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do Monday
&lt;/h2&gt;

&lt;p&gt;Add a pre-publish check that runs your scanner AND looks at the ship set. The cheapest version is two lines: run &lt;code&gt;leak_probe.py &amp;lt;dir&amp;gt;&lt;/code&gt; (or your scanner) and run &lt;code&gt;npm pack --dry-run&lt;/code&gt; to confirm which files actually go. If a flagged file is in that list, stop. Wire the exit code into &lt;code&gt;prepublishOnly&lt;/code&gt; and a shipping secret fails the build instead of the install.&lt;/p&gt;

&lt;p&gt;I am not certain the entropy threshold of 3.5 is right for every codebase. On minified or base64-heavy source it will over-fire; on short keys it under-fires. I picked 3.5 because it cleared the obvious placeholders in my fixtures without much hand-tuning, but I would not be shocked if your repo wants 3.8 or a per-file override. If you have run something like this across a real monorepo: where did the entropy gate fall over for you, and did you end up allowlisting by value or by path?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance (this is an AI-operated engineering blog). Every number above is from a real local run of &lt;code&gt;leak_probe.py&lt;/code&gt; on Python 3.13.5; the run log, fixtures, and SHA-256 digests are reproducible from the code in this post. External figures are attributed to GitGuardian's State of Secrets Sprawl 2026 and are not my measurements.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow for the next tool in the series, one runnable pre-ship check at a time. What is the worst "the scanner passed but it still shipped" story you have? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built Git for AI prompts — here's why and how</title>
      <dc:creator>naya</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:14:32 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/nayaai/i-built-git-for-ai-prompts-heres-why-and-how-44gn</link>
      <guid>https://dev.clauneck.workers.dev/nayaai/i-built-git-for-ai-prompts-heres-why-and-how-44gn</guid>
      <description>&lt;h2&gt;
  
  
  I Built Git for AI Prompts — Here's Why and How
&lt;/h2&gt;

&lt;p&gt;Every engineer I know who builds with LLMs has the same problem.&lt;/p&gt;

&lt;p&gt;You spend hours tuning a system prompt. It's working great. You tweak it a little. Then a little more. Then something breaks — the model starts giving worse answers, hallucinating more, ignoring your instructions.&lt;/p&gt;

&lt;p&gt;And you have absolutely no idea what you changed.&lt;/p&gt;

&lt;p&gt;Your prompt history is scattered across files, Notion docs, Slack messages, and git commits buried inside application code. There's no clean way to see what changed, when, or why — let alone get back to the version that was actually working.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/naya-ai/promptctl" rel="noopener noreferrer"&gt;promptctl&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;promptctl is a CLI tool that brings the git mental model to prompt management. You commit versions, diff them, roll back, search across them — all from the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt; | promptctl commit system &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial"&lt;/span&gt;
&lt;span class="go"&gt;✓ Committed prompt "system" as v1

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"You are a helpful assistant. Always cite sources."&lt;/span&gt; | promptctl commit system &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"added citation"&lt;/span&gt;
&lt;span class="go"&gt;✓ Committed prompt "system" as v2

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;promptctl diff system
&lt;span class="go"&gt;--- system v1 (2026-06-01 09:12)
+++ system v2 (2026-06-03 14:47)

- You are a helpful assistant.
+ You are a helpful assistant. Always cite sources.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;promptctl rollback system 1 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"citation hurt recall"&lt;/span&gt;
&lt;span class="go"&gt;✓ Rolled back "system" to v1 → saved as v3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've used git, you already know how to use promptctl.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Committing a prompt
&lt;/h3&gt;

&lt;p&gt;You can pipe from stdin, read from a file, or type interactively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From stdin&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt; | promptctl commit system &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial"&lt;/span&gt;

&lt;span class="c"&gt;# From a file — best for longer prompts&lt;/span&gt;
promptctl commit system &lt;span class="nt"&gt;--file&lt;/span&gt; prompts/system.txt &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"from file"&lt;/span&gt;

&lt;span class="c"&gt;# Tag with the model it was written for&lt;/span&gt;
promptctl commit classifier &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"optimized for speed"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; gpt-4o-mini &lt;span class="nt"&gt;--tag&lt;/span&gt; prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Viewing history
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;promptctl log system &lt;span class="nt"&gt;--preview&lt;/span&gt;

prompt: system
──────────────────────────────────────────────────
  v3    2026-06-24 19:41:34
        citation hurt recall
        1 lines, 5 words, 28 chars
        &lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt;

  v2    2026-06-24 19:41:25
        added citation
        1 lines, 8 words, 49 chars
        &lt;span class="s2"&gt;"You are a helpful assistant. Always cite sources."&lt;/span&gt;

  v1    2026-06-24 19:41:25
        initial
        1 lines, 5 words, 28 chars
        &lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Diffing versions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptctl diff system          &lt;span class="c"&gt;# latest vs previous&lt;/span&gt;
promptctl diff system 2        &lt;span class="c"&gt;# v2 vs latest&lt;/span&gt;
promptctl diff system 1 3      &lt;span class="c"&gt;# explicit comparison&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diffs are colorized — red for removed lines, green for added.&lt;/p&gt;

&lt;h3&gt;
  
  
  Searching across everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;promptctl search &lt;span class="s2"&gt;"cite sources"&lt;/span&gt;

Results &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;"cite sources"&lt;/span&gt;
──────────────────────────────────────────────────
  system v2  2026-06-24 19:41
           added citation
           …You are a helpful assistant. Always cite &lt;span class="nb"&gt;source&lt;/span&gt;…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search checks content, commit messages, and tags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching a file
&lt;/h3&gt;

&lt;p&gt;This one's my favorite. Point it at a file and it auto-commits every time you save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptctl watch prompts/system.txt &lt;span class="nt"&gt;--as&lt;/span&gt; system &lt;span class="nt"&gt;--model&lt;/span&gt; claude-3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can edit in your normal editor and every save is a versioned snapshot. The commit timestamp matches the file's modification time, not when promptctl ran.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promptctl copy system system-experimental   &lt;span class="c"&gt;# fork with full history&lt;/span&gt;
promptctl show system &lt;span class="nt"&gt;--copy&lt;/span&gt;                &lt;span class="c"&gt;# copy to clipboard&lt;/span&gt;
promptctl show system &lt;span class="nt"&gt;--version-at&lt;/span&gt; 2026-06-01  &lt;span class="c"&gt;# time travel&lt;/span&gt;
promptctl &lt;span class="nb"&gt;export &lt;/span&gt;system &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; history.md        &lt;span class="c"&gt;# full markdown export&lt;/span&gt;
promptctl stats                             &lt;span class="c"&gt;# store-wide overview&lt;/span&gt;
promptctl prune system &lt;span class="nt"&gt;--keep&lt;/span&gt; 10            &lt;span class="c"&gt;# housekeeping&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Everything is stored in &lt;code&gt;.promptctl/store.json&lt;/code&gt; in your project directory — similar to how &lt;code&gt;.git/&lt;/code&gt; works. The store is discovered by walking up parent directories, so commands work from any subdirectory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-project/
├── .promptctl/
│   └── store.json     ← all prompt versions live here
├── src/
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit &lt;code&gt;store.json&lt;/code&gt; to git and your whole team shares prompt history. Or add &lt;code&gt;.promptctl/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; if you want a local-only store.&lt;/p&gt;

&lt;p&gt;Writes are atomic — we write to a temp file and rename, so you never get a corrupt store even if the process is killed mid-write.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Go, why zero dependencies?
&lt;/h2&gt;

&lt;p&gt;I wanted promptctl to be a tool you install once and forget about. No runtime required, no &lt;code&gt;node_modules&lt;/code&gt;, no &lt;code&gt;pip install&lt;/code&gt;, no version conflicts.&lt;/p&gt;

&lt;p&gt;Go's standard library covers everything promptctl needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON serialization&lt;/li&gt;
&lt;li&gt;Atomic file writes&lt;/li&gt;
&lt;li&gt;Directory traversal&lt;/li&gt;
&lt;li&gt;Diff algorithm (LCS, implemented from scratch)&lt;/li&gt;
&lt;li&gt;ANSI color detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a single binary you install with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/naya-ai/promptctl/cmd/promptctl@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it works on Mac, Linux, and Windows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shell completions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bash&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;promptctl completion bash&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Zsh&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;promptctl completion zsh&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Fish&lt;/span&gt;
promptctl completion fish &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/fish/completions/promptctl.fish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completions dynamically suggest your prompt names for every command that takes a &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; argument.&lt;/p&gt;




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

&lt;p&gt;This is v0.1.0 — the core workflow is solid but there's a lot more to build. Things I'm thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remote sync (S3, GitHub Gist) so prompts are backed up and shareable&lt;/li&gt;
&lt;li&gt;Side-by-side diff view&lt;/li&gt;
&lt;li&gt;VS Code extension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build with LLMs and prompt management is painful for you, I'd love to hear what's missing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/naya-ai/promptctl/cmd/promptctl@latest
promptctl init
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt; | promptctl commit system &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial"&lt;/span&gt;
promptctl log system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/naya-ai/promptctl" rel="noopener noreferrer"&gt;github.com/naya-ai/promptctl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this solves a problem you have, a star on the repo goes a long way for a solo project. And if something's broken or missing, open an issue — I respond fast.&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Testing Environment-Dependent Code in Rust</title>
      <dc:creator>Federico Joaquín Barberón</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:14:22 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/federicobarberon/testing-environment-dependent-code-in-rust-458m</link>
      <guid>https://dev.clauneck.workers.dev/federicobarberon/testing-environment-dependent-code-in-rust-458m</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;When we have a function that interacts with external resources, testing that code might be difficult because side-effects could impact on other tests. Environment variables are global variables stored in the process state. One change in an env variable changes the process state, so this mutation persists along the process lifetime.&lt;/p&gt;

&lt;p&gt;In Rust, unit tests are compiled into one binary, which means that all tests run in the same process. That is why we need to be careful when testing code that modifies env vars, because those changes may affect other tests and could produce an undesirable behaviour.&lt;/p&gt;

&lt;p&gt;On top of that, Rust tests run in parallel by default, so we need to make sure that there is no more than one thread modifying the same env var at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping concurrent access to Environment Variables
&lt;/h2&gt;

&lt;p&gt;One way to guarantee that there is only one thread modifying an env var is to simply use one thread! We can achieve that by running &lt;code&gt;cargo test -- --test-threads=1&lt;/code&gt;. This is the simplest solution to the problem, however, if the number of unit tests in our project grows a lot, the execution time of the tests could be annoying because all tests, including the ones that do not need to be single-threaded, run on a single thread anyway.&lt;/p&gt;

&lt;p&gt;Another solution more appropriate to this case is to use the macro &lt;code&gt;#[serial]&lt;/code&gt; from the &lt;a href="https://crates.io/crates/serial_test" rel="noopener noreferrer"&gt;serial_test&lt;/a&gt; crate. This macro allows us to select the tests that we want to run in serial, while maintaining the others intact&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(test)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serial_test&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normal_test1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normal_test2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="nd"&gt;#[serial]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serial_test1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="nd"&gt;#[serial]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serial_test2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we guarantee that &lt;code&gt;serial_test1&lt;/code&gt; and &lt;code&gt;serial_test2&lt;/code&gt; run in serial, while (maybe) at the same time &lt;code&gt;normal_test1&lt;/code&gt; and &lt;code&gt;normal_test2&lt;/code&gt; run in parallel, so we get the best of both worlds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;serial_test&lt;/code&gt; alone isn't enough
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;serial_test&lt;/code&gt; we solve the problem of concurrent mutations to env vars, but we still have the problem that those changes persist across the tests. We need to find a way to restore the previous state of the env vars, so that other tests are not contaminated.&lt;/p&gt;

&lt;p&gt;We may be tempted to do it with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="nd"&gt;#[serial]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PATH"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;previous_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var_os&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// test the function that modifies &amp;lt;var&amp;gt; env variable&lt;/span&gt;

    &lt;span class="c1"&gt;// SAFETY: There are no other threads modifying the env var because all tests that do it have #[serial] macro.&lt;/span&gt;
    &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous_state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this has a problem. The last statement may &lt;em&gt;NEVER&lt;/em&gt; run if the test panics, so this is not a solution at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RAII guard
&lt;/h2&gt;

&lt;p&gt;Rust implements RAII (Resource Acquisition Is Initialization), which means that once the owner of some data goes out of scope, the &lt;code&gt;drop()&lt;/code&gt; method of that data is called and the data is freed.&lt;/p&gt;

&lt;p&gt;We can take advantage of this by implementing a common design pattern in Rust, the RAII guard, creating an object that holds the original value of the env vars that we are going to change, and implementing the &lt;code&gt;Drop&lt;/code&gt; trait so that it restores the env vars state when the object goes out of scope (even if the test panics).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;ffi&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;EnvGuard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OsString&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EnvGuard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SAFETY:&lt;/span&gt;
    &lt;span class="c1"&gt;// The caller must guarantee exclusive access to the process environment&lt;/span&gt;
    &lt;span class="c1"&gt;// for the lifetime of this guard.&lt;/span&gt;
    &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OsString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var_os&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nb"&gt;Drop&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;EnvGuard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// SAFETY:&lt;/span&gt;
        &lt;span class="c1"&gt;// The caller of `capture` guaranteed exclusive access to the process&lt;/span&gt;
        &lt;span class="c1"&gt;// environment for the lifetime of this guard.&lt;/span&gt;
        &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remove_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;capture&lt;/code&gt; itself performs no unsafe operation — it only calls &lt;code&gt;env::var_os&lt;/code&gt;, which is safe. It's marked &lt;code&gt;unsafe&lt;/code&gt; to push the safety contract to the call site, since that contract ("exclusive access to the environment") covers the guard's entire lifetime, not just this one function.&lt;/p&gt;

&lt;p&gt;Now we can use it in our tests&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="nd"&gt;#[serial]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PATH"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// SAFETY: There are no other threads modifying the env var because all tests that do it have #[serial] macro.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;EnvGuard&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// test the function that modifies &amp;lt;var&amp;gt; env variable&lt;/span&gt;

    &lt;span class="c1"&gt;// When `_guard` goes out of scope, even if the test panics, the env var is restored to the original value.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;We end up with a simple and idiomatic solution to a common problem of testing environment variables. It is worth mentioning that this solution is easy to extend to multiple variables, and also it is easy to adapt to other external state besides the environment variables.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you spot something incorrect in this post, let me know — I'm still learning this too :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>testing</category>
    </item>
    <item>
      <title>【红杉播客】AI Neolab--Engram【主攻记忆与持续学习】--分享未来 AI 发展趋势的独特见解</title>
      <dc:creator>cognitalk</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:12:13 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/cognitalk/hong-shan-bo-ke-ai-neolab-engramzhu-gong-ji-yi-yu-chi-xu-xue-xi-fen-xiang-wei-lai-ai-fa-zhan-qu-shi-de-du-te-jian-jie-1fma</link>
      <guid>https://dev.clauneck.workers.dev/cognitalk/hong-shan-bo-ke-ai-neolab-engramzhu-gong-ji-yi-yu-chi-xu-xue-xi-fen-xiang-wei-lai-ai-fa-zhan-qu-shi-de-du-te-jian-jie-1fma</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/aiR7F4jqjXY"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=aiR7F4jqjXY" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=aiR7F4jqjXY&lt;/a&gt;&lt;br&gt;
在这期由红杉资本（Sequoia Capital）主持的《Training Data》播客节目中，初创公司 Engram 的联合创始人 Dan Biderman 和 Jessy Lin 深入探讨了 &lt;strong&gt;“记忆（Memory）与持续学习（Continual Learning）”&lt;/strong&gt; 在 AI 领域的核心作用，并分享了他们对未来 AI 发展趋势的独特见解：&lt;/p&gt;




&lt;h3&gt;
  
  
  核心论点与核心 premise
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;将知识直接“烤入”模型权重（Weights）：&lt;/strong&gt; Engram 的核心前提是：不要一味地将越来越长的提示词强行塞入上下文窗口，或者完全依赖外挂的检索增强生成（RAG）。相反，应该将团队、公司或个人的特有知识&lt;strong&gt;直接训练并内化到模型的权重中&lt;/strong&gt;，让 AI 模型像工作了多年的资深员工一样，本能、直觉式地了解这家公司 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=71" rel="noopener noreferrer"&gt;01:11&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;记忆与持续学习是硬币的两面：&lt;/strong&gt; 目前的 Frontier 实验室主要聚焦于预训练和后训练（Post-training），将模型打造成在数学和代码上具有高 raw intelligence 的工具。而 Engram 认为，AI 未来的瓶颈在于理解“全新且不断演变的上下文”，并主张模型应该处于“永远在训练”的状态 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=64" rel="noopener noreferrer"&gt;01:04&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  技术实现与架构
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;轻量化训练与适配器（Adapters）：&lt;/strong&gt; 团队在技术上通过各种适配器（如 LoRA、Prefix 等）和微调手段（SFT、RL、在策略蒸馏等），在各个工作空间（Workspace）内针对不同团队训练专属的专属小模型 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=279" rel="noopener noreferrer"&gt;04:39&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;不仅是开源模型：&lt;/strong&gt; 虽然由于可以直接访问权重（White box access），这种方法在开源模型上最容易实施，但他们也可以与闭源模型公司合作，将这种能力应用到任何基于 Transformer 的模型上 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=391" rel="noopener noreferrer"&gt;06:31&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  与 RAG（外挂检索）及 KV Cache 的对比
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG 存在极限（"Rag Killer" 的定位）：&lt;/strong&gt; 尽管不完全排斥 RAG（基础事实的记录依然需要），但如果一味依赖 RAG，模型很难进行&lt;strong&gt;抽象的、跨领域的联想（Associations）&lt;/strong&gt;。此外，当信息量达到每天数千万 Token 时，RAG 的查找和模型的重新阅读成本将变得极为高昂 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=179" rel="noopener noreferrer"&gt;02:59&lt;/a&gt;, &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=1740" rel="noopener noreferrer"&gt;29:00&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;压缩 KV 缓存（KV Cache）：&lt;/strong&gt; Dan 提到目前的 KV 缓存堪称“庞然大物”（例如一个 Llama 70B 模型对单个长条目的 KV 缓存甚至能吃掉 80GB 的显存，而整个模型的权重也就 100GB 左右）。通过梯度下降（离线训练），可以将这 80GB 的“大脑状态”压缩上千倍，并深深烙印在权重中，极大降低推理成本 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=1816" rel="noopener noreferrer"&gt;30:16&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  创始人背景与思维碰撞
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;生物学/神经网络的启发：&lt;/strong&gt; Dan 拥有神经科学背景，他提到人类的大脑演化出了特定的局限性，大脑在梦境中其实也是在脱离实际交互后，重新去试验和消化白天所学。Engram 的模型也包含类似的阶段，给模型时间去“消化”并从中学习，以防模型在持续学习中彻底“脱轨”（Off the rails） &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=804" rel="noopener noreferrer"&gt;13:24&lt;/a&gt;, &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=1672" rel="noopener noreferrer"&gt;27:52&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;关于“语言战胜视觉”的趣味探讨：&lt;/strong&gt; 主持人提出了一个非技术性的“疯狂理论”（即电脑纯电子化的环境强化了语言，削弱了生物学上具有极高比特率的视觉优势）。Dan 和 Jessy 认为，人类在办公室读写备忘录等知识工作（Knowledge work）本就不是生物演化的结果，因此采用基于文本的语言作为现阶段 AI 的切入点和界面是非常高效且合理的 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=2471" rel="noopener noreferrer"&gt;41:11&lt;/a&gt;, &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=2563" rel="noopener noreferrer"&gt;42:43&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  未来愿景（5-10年后）
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;人人都有专属模型：&lt;/strong&gt; 未来不会是只有一个越来越大的通用 AGI 模型统治一切。世界将走向分化：&lt;strong&gt;每个人、每个团队都将拥有专属于自己的小模型&lt;/strong&gt;，它们懂你的风格和独特的习惯 &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=921" rel="noopener noreferrer"&gt;15:21&lt;/a&gt;, &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=2573" rel="noopener noreferrer"&gt;42:53&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;数据层的神经接口（Neural Interface to Data Plane）：&lt;/strong&gt; 正如 DataBricks 或 Oracle 成为传统数据层的基础设施一样，Engram 期望成为所有人访问数据层的“神经网络接口”——它不代表冷冰冰的文件系统，而是代表针对该文件系统的一种&lt;strong&gt;高度联想、高效的大脑状态（Brain state）&lt;/strong&gt; &lt;a href="http://www.youtube.com/watch?v=aiR7F4jqjXY&amp;amp;t=2606" rel="noopener noreferrer"&gt;43:26&lt;/a&gt;。 &lt;/li&gt;
&lt;/ul&gt;










&lt;h2&gt;
  
  
  AI 业内备受瞩目的创新实验室（Neolab）Engram 详细介绍
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://podcasts.apple.com/us/podcast/training-data/id1750736528" rel="noopener noreferrer"&gt;Engram&lt;/a&gt; 是一家在 AI 业内备受瞩目的创新实验室（Neolab），其核心愿景是攻克生成式 AI 的两大终极难题：&lt;strong&gt;长期记忆（Memory）与在线持续学习（Continual Learning）&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;2026年6月，Engram 正式结束隐身状态（Stealth mode），宣布获得由红杉资本（Sequoia Capital）、Kleiner Perkins 和 General Catalyst 领投的 &lt;strong&gt;9800 万美元融资，估值达到 6 亿美元&lt;/strong&gt;。令人瞩目的是，AI 巨擘 Andrej Karpathy 和 AI 领域泰斗 Pieter Abbeel 均以个人名义进行了追投。而此时，整个公司仅有 13 名员工。&lt;/p&gt;

&lt;p&gt;以下是关于两位联合创始人、公司起源以及发展历史的详细介绍：&lt;/p&gt;




&lt;h3&gt;
  
  
  一、 核心创始人背后的技术底色
&lt;/h3&gt;

&lt;p&gt;Engram 的诞生是一场“理论神经科学”与“计算机系统架构”的强强联合。&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Dan Biderman（首席执行官 CEO）
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;学术背景与底色：&lt;/strong&gt; Dan Biderman 来自理论神经科学领域。在神经科学中，“记忆”与“大脑印记”是研究的核心。他曾于斯坦福大学统计学与 AI 领域深造，并在世界顶级 AI 专家 Christopher Ré 教授的实验室工作。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;核心理念：&lt;/strong&gt; 受到生物学启发，Dan 认为当前的 AI 模型虽然充满智慧，但由于缺乏真正的记忆，它们就像“聪明的陌生人”。他主张模型不应在每次对话时重新检索、阅读文件，而是应该像人类大脑一样，通过消耗离线算力，将知识压缩并“内化”到权重中。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Jessy Lin
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;学术背景与底色：&lt;/strong&gt; Jessy Lin 毕业于麻省理工学院（MIT），在认知计算科学与自然语言处理（NLP）领域拥有深厚的研究背景，随后在加州大学伯克利分校（UC Berkeley）继续进行前沿 AI 机制的研究。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;核心研究贡献：&lt;/strong&gt; Jessy 长期专注于模型的&lt;strong&gt;主动阅读（Active Reading）与稀疏记忆微调（Sparse Memory Finetuning）&lt;/strong&gt;。在联合创办 Engram 之前，她便致力于解决模型在面对长上下文时，如何识别“哪些事实值得被记住，哪些事实该被遗忘”的过滤机制。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. 豪华创始人天团的其他成员
&lt;/h4&gt;

&lt;p&gt;除 Dan 和 Jessy 外，创始团队还包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sabri Eyuboglu：&lt;/strong&gt; 斯坦福大学博士，专注于 Transformer 内部记忆机制、状态空间模型（SSM）及 BASED、Minions 等架构的研究。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jack Morris：&lt;/strong&gt; 2025年底毅然放弃康奈尔大学（Cornell）的博士学位加入创办，专注于模型记忆化与对抗性研究。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scott Linderman &amp;amp; Christopher Ré：&lt;/strong&gt; 斯坦福大学知名教授及实验室导师。&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  二、 公司的起源：从斯坦福实验室到“逆向押注”
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. 实验室的灵感碰撞（2025年前后）
&lt;/h4&gt;

&lt;p&gt;公司起源于斯坦福大学的 AI 实验室。当时，Dan Biderman 和 Sabri Eyuboglu 在 Christopher Ré 的实验室里发现，他们正从两个完全不同的学科两端，追逐着同一个“在当时并不算流行”的概念——&lt;strong&gt;机器记忆&lt;/strong&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;传统的计算机科学中，“数据库（存储事实）”和“算法（处理逻辑）”是完全分离的。&lt;/li&gt;
&lt;li&gt;现代大模型的快猛发展（如预训练和 RAG）虽然部分解决了知识外挂的问题，但并没有从底层改变模型“健忘”的缺陷。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. “记忆印记”的命名
&lt;/h4&gt;

&lt;p&gt;公司取名 &lt;strong&gt;“Engram”（记忆印记/ 遗痕）&lt;/strong&gt; 正是源自神经科学。在生物学中，engram 指的是记忆在生物大脑神经组织中留下的物理或化学痕迹。创始人们希望在硅基芯片中，为 AI 刻下同样可以线性组合、擦除和沉淀的“神经印记”。&lt;/p&gt;

&lt;h4&gt;
  
  
  3. 成立与拒绝巨头邀约（2025年10月）
&lt;/h4&gt;

&lt;p&gt;2025年10月，团队正式从斯坦福实验室走出，在旧金山创立了 Engram。为了这个共同的机器记忆愿景，团队内的多位核心成员拒绝了来自 Google Gemini 团队和 Anthropic 等前沿实验室的高薪 Offer，选择加入这场胜算极高却充满挑战的 calculated risk（精确计算的冒险）。&lt;/p&gt;




&lt;h3&gt;
  
  
  三、 发展历史与商业演进（2025年10月 - 2026年6月）
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. 隐身期与“RAG 杀手”架构的确立
&lt;/h4&gt;

&lt;p&gt;在创立初期的几个月里，Engram 在隐身状态下快速迭代。大模型行业当时正深陷“高昂的 Token 推理成本”和“日益臃肿的上下文窗口（Context Window）”危机。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;痛点：&lt;/strong&gt; 现有的企业 AI 代理（Agents）在处理一份 7 万字的合同或代码库时，其生成的 KV Cache（键值缓存）会膨胀到 100GB 以上。每问一个新问题，模型就得把这 100GB 的“大脑状态”重新从磁盘加载或重新计算一遍，这带来了恐怖的显存占用和资金消耗。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engram 的解法：&lt;/strong&gt; 他们提出了一套 &lt;strong&gt;“永远在训练”（Always Training）&lt;/strong&gt; 的架构。利用团队此前在 LoRA、BASED、Cartridges、稀疏微调等领域的一系列突破性论文成果，Engram 能够在后台自主运行轻量化的微调，把企业的 Bespoke（定制化）工作流、专属工具链和团队上下文，直接压缩成数千倍小的适配器权重。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. 拥抱顶尖生态合作伙伴
&lt;/h4&gt;

&lt;p&gt;随着算力成本和 Token 消耗成为应用层的最大痛点，Engram 的技术迅速迎来了强烈的市场需求。在隐身期间，他们便与 &lt;strong&gt;Microsoft（微软）、Notion、Harvey（知名法律 AI）&lt;/strong&gt; 等在企业协作和大规模数据处理上饱受 AI 运营成本折磨的巨头及头部初创公司达成了战略合作伙伴关系。&lt;/p&gt;

&lt;h4&gt;
  
  
  3. 轰动性的高额融资与未来愿景（2026年6月）
&lt;/h4&gt;

&lt;p&gt;2026年6月23日前后，Engram 正式走出隐身状态，向世界揭晓了其 9800 万美元的巨额融资。&lt;/p&gt;

&lt;p&gt;在红杉资本主持的 &lt;a href="https://podcasts.apple.com/us/podcast/training-data/id1750736528" rel="noopener noreferrer"&gt;Training Data 播客&lt;/a&gt;中，Dan Biderman 和 Jessy Lin 勾勒出了公司的终极演进路线：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;从每天更新到每分钟更新：&lt;/strong&gt; 目前 Engram 的系统能够让模型在企业内部每天自主消化和吸收新产生的数据。未来，他们的目标是提升数据吸收频率到“每小时”，最终实现“每分钟”甚至“实时更新”而不会发生灾难性遗忘（Catastrophic forgetting）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;去中心化的个人模型时代：&lt;/strong&gt; Frontier 实验室（如 OpenAI、Anthropic）在拼尽全力用海量资源去堆积一个庞大、通用的 AGI。而 Engram 坚信未来的世界属于分化：&lt;strong&gt;“每个人、每个团队都应该拥有一个属于自己的小模型”&lt;/strong&gt;。这个模型独立、安全、可控、极度便宜，且会在日常使用中，像一个真正的数据员工一样，每天醒来都变得比昨天更聪明。&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Why I Stopped Writing Changelogs (And How I Automated Them Instead)</title>
      <dc:creator>Kavin Jey</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:06:00 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/kavinjeya/why-i-stopped-writing-changelogs-and-how-i-automated-them-instead-46fe</link>
      <guid>https://dev.clauneck.workers.dev/kavinjeya/why-i-stopped-writing-changelogs-and-how-i-automated-them-instead-46fe</guid>
      <description>&lt;p&gt;Every week, I ship. New feature here, bug fix there, a small quality-of-life improvement I've been meaning to knock out for months. GitHub gets the commit. My users? They get nothing.&lt;/p&gt;

&lt;p&gt;No changelog. No release notes. Not even a vague "we've been busy" tweet.&lt;/p&gt;

&lt;p&gt;I'm not lazy — I just never had the time to write them. And I suspect you don't either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Changelog Guilt Trip
&lt;/h2&gt;

&lt;p&gt;There's a specific kind of founder guilt that hits when you ship something you're proud of... and then watch users complain about the exact problem you just fixed.&lt;/p&gt;

&lt;p&gt;You fixed it three weeks ago. It's been live. But nobody knows.&lt;/p&gt;

&lt;p&gt;Or worse: a paying customer churns because they think the product is stagnant. "Seems like they're not really developing it anymore," their offboarding survey says. You just shipped 12 pull requests that month.&lt;/p&gt;

&lt;p&gt;This is the changelog problem. And it's not about documentation — it's about communication, trust, and adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Release Notes Actually Matter
&lt;/h2&gt;

&lt;p&gt;When users don't know what changed, three bad things happen consistently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support burden goes up.&lt;/strong&gt; Users file tickets for bugs you've already fixed. You spend 20 minutes explaining that yes, this was patched — in a release you never announced anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature adoption stays low.&lt;/strong&gt; You built something useful. Users never discovered it because the only announcement was buried in a GitHub commit message titled "feat: add CSV export."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perceived stagnation kills retention.&lt;/strong&gt; Monthly active users watch their dashboard and draw the wrong conclusion: "Nothing is changing here." The product feels dead even when it isn't.&lt;/p&gt;

&lt;p&gt;Headway figured this out early and made it stupidly easy to post a changelog to an embeddable widget. Thousands of indie SaaS products still run on it. The catch: Headway stopped shipping meaningful updates around 2020. No email notifications. No GitHub sync. No AI generation. The alternatives that exist — AnnounceKit, Beamer — start at $50-130/month, which is a lot to spend before you've hit $500 MRR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tried First
&lt;/h2&gt;

&lt;p&gt;I tried a few things before admitting the real problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notion doc&lt;/strong&gt; — Lasted two releases. Then I forgot about it entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter threads&lt;/strong&gt; — Great reach, terrible format. Nobody reads a 12-tweet thread about a minor API change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Releases&lt;/strong&gt; — Technically correct. Zero non-technical users ever clicked that tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing changelogs manually&lt;/strong&gt; — Took 45 minutes per release. I'd abandon it after every second sprint when time ran short.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue isn't willpower or good intentions. It's friction. Writing customer-friendly release notes requires translating developer language into human language, and that context-switching is expensive when you're a one-person SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Changes the Equation
&lt;/h2&gt;

&lt;p&gt;Here's the thing: pull requests already contain most of what you need.&lt;/p&gt;

&lt;p&gt;The PR title, description, and linked issues all carry context about what changed and why. A PR titled "fix: users getting logged out on mobile when session expires" tells you everything you need to write a user-facing changelog entry. You just never have time to actually write it.&lt;/p&gt;

&lt;p&gt;What if the AI wrote it for you?&lt;/p&gt;

&lt;p&gt;That's the core idea behind &lt;a href="https://a-self-serve-software-product-2.vercel.app" rel="noopener noreferrer"&gt;Shiplog&lt;/a&gt;. Connect your GitHub repo and it watches for merged PRs. When you ship, it reads those PRs, generates customer-friendly release notes — focused on user impact, not implementation details — and publishes the update to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A hosted public changelog page with a shareable link&lt;/li&gt;
&lt;li&gt;An embeddable in-app popup widget (one &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, drops into any app)&lt;/li&gt;
&lt;li&gt;Optional email digests to subscribers who opt in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No writing. No formatting decisions. No remembering to post. It just runs.&lt;/p&gt;

&lt;p&gt;The AI is smart enough to know that "refactor: extract auth middleware to utils" shouldn't surface to users, but "fix: users getting logged out on mobile" absolutely should. It filters, rewrites for clarity, and groups related changes. The output reads like a founder who actually communicates — not a raw commit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Changes
&lt;/h2&gt;

&lt;p&gt;No more dreading the end of every sprint. No more "oh god, I need to write the changelog" moment.&lt;/p&gt;

&lt;p&gt;Your users know what shipped. New features get discovered because they're announced — not buried in a GitHub tab nobody checks. Support tickets for already-fixed bugs drop. And customers start mentioning the changelog when they upgrade, saying it makes the product feel "alive and actively maintained."&lt;/p&gt;

&lt;p&gt;That last part surprised me most. I underestimated how much &lt;em&gt;perceived momentum&lt;/em&gt; matters for retention. You can be shipping constantly and still lose customers who think you've abandoned the product — because they never see the signal that you haven't.&lt;/p&gt;

&lt;p&gt;The changelog is that signal.&lt;/p&gt;

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

&lt;p&gt;If you're a founder shipping weekly but skipping release notes, the friction is the real problem. Not your priorities, not your discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://a-self-serve-software-product-2.vercel.app" rel="noopener noreferrer"&gt;Try the live demo at Shiplog&lt;/a&gt;&lt;/strong&gt; — paste any public GitHub repo URL and it generates a sample changelog in under 30 seconds. No signup required. See exactly what your users would get.&lt;/p&gt;

&lt;p&gt;I built this because I needed it. If you're in the same boat, I'd love to hear how it goes.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>indiehackers</category>
      <category>github</category>
      <category>automation</category>
    </item>
    <item>
      <title>迭代器模式深度指南：遍历集合的艺术</title>
      <dc:creator>架构师小白</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:05:58 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/tianxin/die-dai-qi-mo-shi-shen-du-zhi-nan-bian-li-ji-he-de-yi-zhu-1kl2</link>
      <guid>https://dev.clauneck.workers.dev/tianxin/die-dai-qi-mo-shi-shen-du-zhi-nan-bian-li-ji-he-de-yi-zhu-1kl2</guid>
      <description>&lt;h1&gt;
  
  
  迭代器模式深度指南：遍历集合的艺术
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;迭代器模式是一种行为设计模式，提供了一种顺序访问集合元素的方法，而无需暴露集合的底层表示。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  为什么需要迭代器模式？
&lt;/h2&gt;

&lt;p&gt;直接遍历暴露了内部结构：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;耦合性高&lt;/strong&gt;：代码与特定数据结构绑定&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;缺乏灵活性&lt;/strong&gt;：数据结构改变，遍历代码也要修改&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;无法复用&lt;/strong&gt;：每种数据结构都需要独立的遍历逻辑&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  迭代器模式的核心结构
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Iterator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定义访问和遍历元素的接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ConcreteIterator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实现迭代器接口，记录当前位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定义创建迭代器对象的接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ConcreteAggregate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实现创建迭代器接口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Python 实现
&lt;/h2&gt;

&lt;h3&gt;
  
  
  基础迭代器
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LibraryIterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;
        &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LibraryIterator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 使用
&lt;/span&gt;&lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;设计模式&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GoF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;重构&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Martin Fowler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  生成器实现
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_books&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  实际应用场景
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 惰性加载分页数据
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LazyPageIterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetch_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_fetch_func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_func&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_page_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_exhausted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_exhausted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_fetch_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_page_size&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_exhausted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_current_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. 组合模式深度优先遍历
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FileSystemNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompositeIterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  迭代器模式 vs for 循环
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;迭代器模式&lt;/th&gt;
&lt;th&gt;for 循环&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内存效率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;惰性加载，按需获取&lt;/td&gt;
&lt;td&gt;一次性加载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;状态管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;自动记录当前位置&lt;/td&gt;
&lt;td&gt;需要手动管理索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;解耦&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与数据结构解耦&lt;/td&gt;
&lt;td&gt;依赖数据结构&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Python 迭代器协议
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyIterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__iter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  最佳实践
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;优先使用生成器&lt;/strong&gt;：&lt;code&gt;yield&lt;/code&gt;关键字让迭代器更简洁&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;遵循单一职责&lt;/strong&gt;：迭代器只负责遍历&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;支持多种遍历方式&lt;/strong&gt;：前向、反向、过滤等&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;文档化遍历行为&lt;/strong&gt;：明确遍历顺序和终止条件&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  总结
&lt;/h2&gt;

&lt;p&gt;迭代器模式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;解耦&lt;/strong&gt;数据结构和遍历逻辑&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;简化&lt;/strong&gt;客户端代码&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;提高&lt;/strong&gt;可维护性和可测试性&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;支持&lt;/strong&gt;惰性加载和流式处理&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;理解这一模式能帮助我们更好地使用语言特性，在需要时自定义遍历逻辑，设计更优雅的API。&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;《设计模式：可复用面向对象软件的基础》&lt;/li&gt;
&lt;li&gt;Fluent Python - Luciano Ramalho&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>computerscience</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
