<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent Discussions</title>
<link>https://coderlegion.com/qa/discussions</link>
<description>Powered by Question2Answer</description>
<item>
<title>Answered: How Much Detail Belongs in an Error Message?</title>
<link>https://coderlegion.com/32826/how-much-detail-belongs-in-an-error-message?show=33335#a33335</link>
<description>&lt;p&gt;I think the best approach is to keep API errors technical internally but make the frontend messages simple and actionable. Users need to know what went wrong and how to fix it, not see the entire response payload. A support reference ID is also a smart way to make debugging easier without exposing sensitive details.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32826/how-much-detail-belongs-in-an-error-message?show=33335#a33335</guid>
<pubDate>Sun, 01 Nov 2026 16:17:49 +0000</pubDate>
</item>
<item>
<title>Answered: OPEN-SOURCE ATTENDNACE SYSTEM FOR AN SME</title>
<link>https://coderlegion.com/33294/open-source-attendnace-system-for-an-sme?show=33306#a33306</link>
<description>&lt;p&gt;Interesting idea for small businesses. An open source attendance system can save SMEs a lot of money while making employee tracking much easier. I’d love to see features like leave management, late entry reports, and simple monthly attendance exports.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/33294/open-source-attendnace-system-for-an-sme?show=33306#a33306</guid>
<pubDate>Sun, 01 Nov 2026 12:06:09 +0000</pubDate>
</item>
<item>
<title>Answered: What Should a Permission Error Explain?</title>
<link>https://coderlegion.com/32825/what-should-a-permission-error-explain?show=33295#a33295</link>
<description>&lt;p&gt;Good permission errors should answer: Why am I blocked? Can I fix it? What’s the next step? without exposing sensitive details about owners, groups, or permissions.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32825/what-should-a-permission-error-explain?show=33295#a33295</guid>
<pubDate>Sun, 01 Nov 2026 08:34:12 +0000</pubDate>
</item>
<item>
<title>Answered: Should Exact Search Matches Always Come First?</title>
<link>https://coderlegion.com/32824/should-exact-search-matches-always-come-first?show=33115#a33115</link>
<description>&lt;p&gt;I think exact matches should generally come first because users often search with a specific item in mind. However, recent activity and relevance should still influence the ranking, especially when multiple results have similar names. Would an option to prioritize exact matches make the search experience more flexible without adding unnecessary complexity?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32824/should-exact-search-matches-always-come-first?show=33115#a33115</guid>
<pubDate>Fri, 30 Oct 2026 15:36:43 +0000</pubDate>
</item>
<item>
<title>Answered: Which Time Zone Is This Export Using?</title>
<link>https://coderlegion.com/32822/which-time-zone-is-this-export-using?show=32996#a32996</link>
<description>&lt;p&gt;Great reminder that timezone handling can easily cause confusion in exports. Including the timezone and UTC offset would make things much clearer for everyone.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32822/which-time-zone-is-this-export-using?show=32996#a32996</guid>
<pubDate>Thu, 29 Oct 2026 16:23:39 +0000</pubDate>
</item>
<item>
<title>Answered: When should an AI agent stop acting?</title>
<link>https://coderlegion.com/32933/when-should-an-ai-agent-stop-acting?show=32984#a32984</link>
<description>&lt;p&gt;I stop an agent after the second failed retry on the same resource. The first retry rules out a transient error, but after that it should post the logs and the last error and wait. Looping past that point burns quota and hides the real fault.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32933/when-should-an-ai-agent-stop-acting?show=32984#a32984</guid>
<pubDate>Thu, 29 Oct 2026 14:08:53 +0000</pubDate>
</item>
<item>
<title>Answered: AI Gives the Right Answer. But Does It Actually Save Time?</title>
<link>https://coderlegion.com/32840/ai-gives-the-right-answer-but-does-it-actually-save-time?show=32981#a32981</link>
<description>&lt;p&gt;Integration has been harder for me, and the copy-and-update step you describe is where the time goes. I'd log what happens to each answer once it lands in the record, saved as is or edited first. That edit rate answers your title question more directly than an accuracy score, because it counts the work left for the user.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32840/ai-gives-the-right-answer-but-does-it-actually-save-time?show=32981#a32981</guid>
<pubDate>Thu, 29 Oct 2026 13:57:03 +0000</pubDate>
</item>
<item>
<title>Answered: Senior Engineers Are Paid to Say No</title>
<link>https://coderlegion.com/32843/senior-engineers-are-paid-to-say-no?show=32870#a32870</link>
<description>&lt;p&gt;Knowing when to say no is an important engineering skill, especially when a quick solution could create bigger problems later. How do you balance pushing back on unrealistic requests while still meeting business goals and deadlines?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32843/senior-engineers-are-paid-to-say-no?show=32870#a32870</guid>
<pubDate>Wed, 28 Oct 2026 16:49:42 +0000</pubDate>
</item>
<item>
<title>Answered: Should a Failed Upload Clear the Form?</title>
<link>https://coderlegion.com/32810/should-a-failed-upload-clear-the-form?show=32869#a32869</link>
<description>&lt;p&gt;I think a failed upload should keep the form data whenever possible, so users don’t have to start over and lose their progress. How would you handle cases where some files upload successfully but others fail?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32810/should-a-failed-upload-clear-the-form?show=32869#a32869</guid>
<pubDate>Wed, 28 Oct 2026 16:49:09 +0000</pubDate>
</item>
<item>
<title>Answered: Build an AI Ops Dashboard with FSCSS modules (ai-ui, st-core, circle-progress, overlapui)</title>
<link>https://coderlegion.com/32863/build-an-ai-ops-dashboard-with-fscss-modules-ai-ui-st-core-circle-progress-overlapui?show=32866#a32866</link>
<description>&lt;p&gt;Nice project. Building an AI ops dashboard with reusable components and CSS modules is a practical approach. I especially like the focus on UI structure and handling overlapping progress circles. Clean, reusable components make dashboards much easier to maintain as they grow.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32863/build-an-ai-ops-dashboard-with-fscss-modules-ai-ui-st-core-circle-progress-overlapui?show=32866#a32866</guid>
<pubDate>Wed, 28 Oct 2026 16:25:21 +0000</pubDate>
</item>
<item>
<title>Answered: I’m back, and this time we’re coming out of the casino, not the mine! 😎</title>
<link>https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine?show=32788#a32788</link>
<description>&lt;p&gt;Really interesting title. I like the idea of changing direction and trying a different approach. Looking forward to seeing what comes next.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine?show=32788#a32788</guid>
<pubDate>Wed, 28 Oct 2026 09:29:24 +0000</pubDate>
</item>
<item>
<title>Answered: What Does an Empty Dashboard Mean?</title>
<link>https://coderlegion.com/32662/what-does-an-empty-dashboard-mean?show=32668#a32668</link>
<description>&lt;p&gt;An empty dashboard doesn’t always mean something is missing. Sometimes the real issue is how the system communicates with users. Clear messages, useful guidance, and a suggested next step can make a big difference in user experience. I think designing for these situations is just as important as designing the dashboard itself.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32662/what-does-an-empty-dashboard-mean?show=32668#a32668</guid>
<pubDate>Tue, 27 Oct 2026 11:51:53 +0000</pubDate>
</item>
<item>
<title>Answered: When Should Delete Offer Undo?</title>
<link>https://coderlegion.com/32495/when-should-delete-offer-undo?show=32539#a32539</link>
<description>&lt;p&gt;Undo is such a small feature, but it can make a big difference in user experience. Giving users a chance to recover from mistakes makes an app feel much more reliable.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32495/when-should-delete-offer-undo?show=32539#a32539</guid>
<pubDate>Mon, 26 Oct 2026 12:20:57 +0000</pubDate>
</item>
<item>
<title>Answered: What part of debugging does AI still struggle with most?</title>
<link>https://coderlegion.com/31582/what-part-of-debugging-does-ai-still-struggle-with-most?show=32417#a32417</link>
<description>&lt;p&gt;From my experience, despite how creative AI can be, it still fails to translate our mental process into what its actual output becomes. Because humans and AI aren't fully in sync, debugging will always be part of the mandate. We picture perfection, but we're flawed in how we communicate it.&lt;/p&gt;
&lt;p&gt;So the real question becomes: What communication process are we missing that makes post-hoc debugging the only way to clean up the gap?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31582/what-part-of-debugging-does-ai-still-struggle-with-most?show=32417#a32417</guid>
<pubDate>Sun, 25 Oct 2026 16:04:52 +0000</pubDate>
</item>
<item>
<title>Answered: 💬 Developers: Would you ship AI-generated code without reviewing it first?</title>
<link>https://coderlegion.com/31675/developers-would-you-ship-ai-generated-code-without-reviewing-it-first?show=32415#a32415</link>
<description>&lt;p&gt;One word bro.... or let's make it two - No, Never!&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31675/developers-would-you-ship-ai-generated-code-without-reviewing-it-first?show=32415#a32415</guid>
<pubDate>Sun, 25 Oct 2026 15:59:14 +0000</pubDate>
</item>
<item>
<title>Answered: Help, where do I start?  Animated Character videos</title>
<link>https://coderlegion.com/31847/help-where-do-i-start-animated-character-videos?show=32414#a32414</link>
<description>&lt;p&gt;This is a Long one tho but you're going to have to bear with me 😅&lt;/p&gt;
&lt;p&gt;The overwhelm is completely valid, the AI video landscape is full of bloatware, and many general-purpose LLMs (like ChatGPT or Claude) only help write scripts, not render consistent video scenes.&lt;/p&gt;
&lt;p&gt;To keep your budget intact and avoid subscription burn, the key is knowing that &lt;strong&gt;no single tool does everything well&lt;/strong&gt;. You need a simple, targeted pipeline:&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-practical-3-step-pipeline&quot;&gt;The Practical 3-Step Pipeline&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Step 1: Character &amp;amp; Style Anchor (2D / Illustrated)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Goal:&lt;/strong&gt; Lock in one visual character turnaround (front, side, close-up) so your character doesn't morph between shots.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;strong&gt;Midjourney&lt;/strong&gt; (using character reference tags &lt;code&gt;--cref&lt;/code&gt;) or tools designed for identity locking like &lt;strong&gt;Neolemon&lt;/strong&gt; or &lt;strong&gt;Paintbrush&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Avoid:&lt;/em&gt; Direct text-to-video for new characters—it causes instant visual drift.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Step 2: Voice &amp;amp; Lip-Sync Audio&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Goal:&lt;/strong&gt; Get high-quality, expressive voice lines before doing any animation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;strong&gt;ElevenLabs&lt;/strong&gt; is the industry baseline for character voice acting. Generates crisp audio clips you can feed directly into video lip-sync engines.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Step 3: Image-to-Video Animation&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Goal:&lt;/strong&gt; Animate your anchored still images using audio or motion prompts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;strong&gt;Runway (Gen-4/Gen-3)&lt;/strong&gt;, &lt;strong&gt;Kling AI&lt;/strong&gt;, or &lt;strong&gt;Seedance&lt;/strong&gt; via image-to-video. Upload your character image, add the ElevenLabs voice, and let the tool drive the talking head/expressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-advice-before-you-spend&quot;&gt;Key Advice Before You Spend&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Distinguish &quot;Patch/Clip&quot; from &quot;Project&quot;:&lt;/strong&gt; Don't try to generate a full 3-minute video in one prompt. Build scene-by-scene, shot-by-shot (3–5 second clips max per prompt) and edit them together in a standard video editor like CapCut or Premiere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start on Free / Entry Tiers:&lt;/strong&gt; Run test clips using free daily credits on Runway or Kling to test face stability before committing to paid creator plans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tutorial Recommendation:&lt;/strong&gt; Look up YouTube tutorials specifically searching for &lt;strong&gt;&quot;AI Image-to-Video Character Consistency Workflow&quot;&lt;/strong&gt; or &lt;strong&gt;&quot;Midjourney + ElevenLabs + Runway Lip Sync Tutorial&quot;&lt;/strong&gt;. These break down shot composition without selling platform fluff.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;What specific illustration style (e.g., 2D anime, comic book, 3D toon) are you aiming for with your characters?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31847/help-where-do-i-start-animated-character-videos?show=32414#a32414</guid>
<pubDate>Sun, 25 Oct 2026 15:57:24 +0000</pubDate>
</item>
<item>
<title>Answered: AI Is Not Going to Save You. It Might Not Replace You Either.</title>
<link>https://coderlegion.com/32203/ai-is-not-going-to-save-you-it-might-not-replace-you-either?show=32413#a32413</link>
<description>&lt;p&gt;&quot;The real product is not the code. It is the behavior of the system over time.&quot;&lt;/p&gt;
&lt;p&gt;This hits the nail on the head. AI drastically changes the cost of typing syntax, but it doesn't touch accountability, system architecture, or the reality of debugging production failures when things break. A fantastic reminder that our real job is cultivating judgment, not just churning out volume.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32203/ai-is-not-going-to-save-you-it-might-not-replace-you-either?show=32413#a32413</guid>
<pubDate>Sun, 25 Oct 2026 15:54:08 +0000</pubDate>
</item>
<item>
<title>Answered: I'm Still Figuring This Out</title>
<link>https://coderlegion.com/31426/im-still-figuring-this-out?show=32412#a32412</link>
<description>&lt;p&gt;Love this perspective. Reclaiming the joy of building by leaning into architecture, problem-solving, and agentic workflows—rather than getting bogged down in implementation boilerplate—is a massive shift.&lt;/p&gt;
&lt;p&gt;Looking forward to following the work with NotableBIT and BIT Voices as you build in public! 🙌&lt;/p&gt;
&lt;p&gt;What do you think of this draft? Let me know if you want to tweak the tone or focus!&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31426/im-still-figuring-this-out?show=32412#a32412</guid>
<pubDate>Sun, 25 Oct 2026 15:44:59 +0000</pubDate>
</item>
<item>
<title>Answered: What evidence should an AI coding assistant provide before saying a bug is fixed?</title>
<link>https://coderlegion.com/31594/what-evidence-should-an-ai-coding-assistant-provide-before-saying-a-bug-is-fixed?show=32410#a32410</link>
<description>&lt;p&gt;Setting a risk-tiered standard makes total sense: a static typo or label fix only needs a simple visual diff, while race conditions or core logic shifts demand a strict failing-to-passing reproduction test.&lt;/p&gt;
&lt;p&gt;I'd lean toward requiring a regression test for anything touching state or business logic, but allowing a documented manual check for trivial UI or copy updates. Explicitly separating states like &quot;patch ready,&quot; &quot;locally verified,&quot; and &quot;production confirmed&quot; in AI tooling would clear up so much friction during code reviews.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31594/what-evidence-should-an-ai-coding-assistant-provide-before-saying-a-bug-is-fixed?show=32410#a32410</guid>
<pubDate>Sun, 25 Oct 2026 15:42:36 +0000</pubDate>
</item>
<item>
<title>Answered: When Is a Progress Bar Misleading?</title>
<link>https://coderlegion.com/32377/when-is-a-progress-bar-misleading?show=32394#a32394</link>
<description>&lt;p&gt;A progress bar can sometimes create more frustration than confidence if it doesn’t reflect what’s actually happening. I think showing an honest status message is better than pretending to know the exact completion time. But is a fake percentage ever acceptable if it helps users feel the process is moving forward?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32377/when-is-a-progress-bar-misleading?show=32394#a32394</guid>
<pubDate>Sun, 25 Oct 2026 13:09:57 +0000</pubDate>
</item>
<item>
<title>Answered: When Should a Client Preview Link Expire?</title>
<link>https://coderlegion.com/32262/when-should-a-client-preview-link-expire?show=32365#a32365</link>
<description>&lt;p&gt;Love the clarifications in comments. Thanks for the assistance!&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32262/when-should-a-client-preview-link-expire?show=32365#a32365</guid>
<pubDate>Sun, 25 Oct 2026 05:54:19 +0000</pubDate>
</item>
<item>
<title>Answered: Can You Belong to a Community Without Finding Your Audience?</title>
<link>https://coderlegion.com/32113/can-you-belong-to-a-community-without-finding-your-audience?show=32314#a32314</link>
<description>&lt;p&gt;I find CoderLegion to be  a project without clear direction or focus. It is equal time LinkedIn, Craig's List, Twitter, Medium and perhaps more. In all these areas, it is not a platform for consumers - it is a platform for other technical content creators. Finding an actual community for any game here will be hard. While the total user base is smaller, and therefore any project is easier to discover, it is most likely going to be discovered by other creators also looking for &lt;strong&gt;their&lt;/strong&gt; audience.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32113/can-you-belong-to-a-community-without-finding-your-audience?show=32314#a32314</guid>
<pubDate>Sat, 24 Oct 2026 16:51:53 +0000</pubDate>
</item>
<item>
<title>Answered: Building in Public From a Solo Gamedev Perspective (credits to @Karol Modelski for the post idea)</title>
<link>https://coderlegion.com/31555/building-public-from-solo-gamedev-perspective-credits-karol-modelski-for-the-post-idea?show=32313#a32313</link>
<description>&lt;p&gt;I often question what is the real reason for &quot;building in public&quot;. gamedev/indiedev/screenshotsaturday hashtags are moreless an echo chamber where the main consumers of those posts are not players from the target audience but rather fellow gamedevs who might never play the game.&lt;/p&gt;
&lt;p&gt;The progress updates are then mainly about getting some encouragement to further development via likes and other forms of interactions, feedback (as well-intentioned as it might be) shall be scrutinized and filtered.&lt;/p&gt;
&lt;p&gt;Thinking about it in this way might alleviate some stress from the posting process itself.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31555/building-public-from-solo-gamedev-perspective-credits-karol-modelski-for-the-post-idea?show=32313#a32313</guid>
<pubDate>Sat, 24 Oct 2026 16:45:58 +0000</pubDate>
</item>
<item>
<title>Answered: Should a Resized Image Export Need a Fresh Visual Review?</title>
<link>https://coderlegion.com/32193/should-a-resized-image-export-need-a-fresh-visual-review?show=32307#a32307</link>
<description>&lt;p&gt;I would make the boundary a rule that can be checked by machine instead of a policy: the derivative must keep the approved crop box and aspect ratio, and the disclosure label region must stay above a minimum rendered height, for example measured in pixels at the delivery size. If both checks pass, skip human review and write the export record; if either fails, require a visual check. A label that is readable at 2000 px can fail at 800 px, and a digest cannot see that. What delivery sizes does the pipeline produce, and has a label ever passed review at full size and failed at the smallest one?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32193/should-a-resized-image-export-need-a-fresh-visual-review?show=32307#a32307</guid>
<pubDate>Sat, 24 Oct 2026 16:21:03 +0000</pubDate>
</item>
<item>
<title>Answered: When an AI Image Job Times Out, Should Retry Create a New Job?</title>
<link>https://coderlegion.com/31954/when-an-ai-image-job-times-out-should-retry-create-a-new-job?show=32064#a32064</link>
<description>&lt;p&gt;The case I'd design for next is the browser losing the operation ID entirely, say a cleared tab or the photographer retrying from a phone. I'd have the server list in-flight batches for the account and show them on the upload screen before Retry is even offered, so recovery doesn't depend on client storage. I'd also hang the credit reservation off the operation ID and release it if no job record appears within a short window, which covers your &quot;credits possibly reserved&quot; case where nothing actually ran.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31954/when-an-ai-image-job-times-out-should-retry-create-a-new-job?show=32064#a32064</guid>
<pubDate>Thu, 22 Oct 2026 11:47:18 +0000</pubDate>
</item>
<item>
<title>Answered: php2shell: how are you confirming which PHP your web traffic actually runs on?</title>
<link>https://coderlegion.com/32010/php2shell-how-are-you-confirming-which-php-your-web-traffic-actually-runs-on?show=32052#a32052</link>
<description>&lt;p&gt;Interesting point. Checking &lt;code&gt;php -v&lt;/code&gt; isn’t always enough because the web server might be running a different PHP version. How do you verify the actual version handling live traffic across all environments?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32010/php2shell-how-are-you-confirming-which-php-your-web-traffic-actually-runs-on?show=32052#a32052</guid>
<pubDate>Thu, 22 Oct 2026 09:21:09 +0000</pubDate>
</item>
<item>
<title>Answered: Mapping 32k+ MCP Servers: How we are handling tool discovery without schema drift</title>
<link>https://coderlegion.com/31988/mapping-32k-mcp-servers-how-we-are-handling-tool-discovery-without-schema-drift?show=32036#a32036</link>
<description>&lt;p&gt;Great questions @SuMTa and @scunliffe! &lt;/p&gt;
&lt;p&gt;Handling 32K+ independently updating MCP servers without triggering context-window drift or JSON-RPC tool lookup failures requires moving away from static tool manifests. &lt;/p&gt;
&lt;p&gt;In our architecture (deployed across the SEOSiri MCP Suite at developers.seosiri.com), we solve this using a three-tiered defense:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dynamic JSON-RPC 2.0 Introspection (&lt;code&gt;tools/list&lt;/code&gt;): Rather than hardcoding tool definitions in the client prompt, local agents dynamically query the edge gateway on session handshake. This ensures any backend parameter update is reflected instantly.&lt;/li&gt;
&lt;li&gt;Cryptographic Schema Sealing (HMAC-SHA256): To prevent man-in-the-middle schema substitution or payload drift, every tool declaration and execution block is cryptographically signed at the edge (running on Cloudflare Workers). If a tool's schema drifts or fails validation, the gateway returns a strict JSON-RPC error code before it ever hits the LLM context window.&lt;/li&gt;
&lt;li&gt;Stateless Edge Buffer Tiering: High-frequency tool telemetry uses a hot/cold RAM separation model, ensuring token consumption stays minimal while maintaining sub-millisecond discovery latency.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Are you running into schema versioning issues with custom local servers, or scaling across multi-agent enterprise mesh layers? Let's discuss how you're handling state sync!&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31988/mapping-32k-mcp-servers-how-we-are-handling-tool-discovery-without-schema-drift?show=32036#a32036</guid>
<pubDate>Thu, 22 Oct 2026 07:33:23 +0000</pubDate>
</item>
<item>
<title>Introducing The Startup Café: A Community for People Building Startups ☕</title>
<link>https://coderlegion.com/31883/introducing-the-startup-cafe-a-community-for-people-building-startups</link>
<description>&lt;p&gt;&lt;strong&gt;I Started The Startup Café on DEV. Here's the Idea Behind It.&lt;/strong&gt;&lt;br&gt;
I've just started a new organization on DEV called &lt;strong&gt;The Startup Café&lt;/strong&gt;, and I wanted to share it here while it's still at the very beginning.&lt;/p&gt;
&lt;p&gt;Here's the organization: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/startupcafe&quot;&gt;https://dev.to/startupcafe&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The idea came from something pretty simple: there's a lot to learn from people who are building things, even when they're working on completely different projects.&lt;/p&gt;
&lt;p&gt;Some developers build things for clients. Some work on side projects after their day job. Some are trying to launch a SaaS, while others are still figuring out which idea is worth pursuing.&lt;/p&gt;
&lt;p&gt;I've always found that side of building interesting. Not just seeing what someone has launched, but understanding why they started, what they're figuring out, and what they've learned along the way.&lt;/p&gt;
&lt;p&gt;I'd like The Startup Café to be a place for those conversations.&lt;/p&gt;
&lt;h3 id=&quot;what-do-i-have-in-mind-&quot;&gt;What do I have in mind?&lt;/h3&gt;
&lt;p&gt;I don't want this to be just another place to drop a project link and move on.&lt;/p&gt;
&lt;p&gt;I'd like people to share what they're working on, ask for feedback when they're stuck, write about things they've learned, and discover projects they might not have found otherwise.&lt;/p&gt;
&lt;p&gt;Maybe you're building your first website. Maybe you're working on a SaaS and trying to find your first users. Maybe you've got an open-source project, or an idea you keep coming back to but haven't started yet.&lt;/p&gt;
&lt;p&gt;All of those are worth talking about.&lt;/p&gt;
&lt;p&gt;You don't need to have a successful startup or years of experience to take part. Sometimes the most interesting discussions come from people who are still working things out.&lt;/p&gt;
&lt;p&gt;I'd also love to see people connect through the organization. You might find someone dealing with the same problem, discover a project you want to contribute to, or meet a developer who's interested in building something similar.&lt;/p&gt;
&lt;p&gt;There's no guarantee of where those conversations will lead, but that's part of what makes a community interesting.&lt;/p&gt;
&lt;h3 id=&quot;it-s-just-getting-started&quot;&gt;It's just getting started&lt;/h3&gt;
&lt;p&gt;I want to be clear that The Startup Café is brand new. I'm not introducing an established community with a big audience or claiming we've already built something huge.&lt;/p&gt;
&lt;p&gt;I'm starting small, and I'd like the people who join to have a say in what this becomes.&lt;/p&gt;
&lt;p&gt;What would make it useful? What kinds of posts would you like to see? Would you be interested in sharing your progress, getting feedback, finding collaborators, or learning from other people's experiences?&lt;/p&gt;
&lt;p&gt;Those are the kinds of questions I'd like us to figure out together.&lt;/p&gt;
&lt;h3 id=&quot;interested-in-joining-&quot;&gt;Interested in joining?&lt;/h3&gt;
&lt;p&gt;The Startup Café is a &lt;strong&gt;DEV.to organization&lt;/strong&gt;, and that's where the community will come together.&lt;/p&gt;
&lt;p&gt;You can visit the organization here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/startupcafe&quot;&gt;https://dev.to/startupcafe&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I've also built a small website for it. If you'd like to join, you can visit the website to learn more and submit an application:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://thestartupcafe.vercel.app&quot;&gt;https://thestartupcafe.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If this sounds like something you'd enjoy being part of, I'd be glad to have you check it out.&lt;/p&gt;
&lt;p&gt;And even if you're not ready to join, I'd still appreciate hearing your thoughts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So, what are you building these days?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It can be a startup, a side project, an open-source contribution, or just an idea you're considering. I'd love to hear what you're working on and where you are with it.&lt;/p&gt;
&lt;p&gt;Let's start there.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31883/introducing-the-startup-cafe-a-community-for-people-building-startups</guid>
<pubDate>Tue, 20 Oct 2026 19:15:24 +0000</pubDate>
</item>
<item>
<title>Should an AI Listing Draft Be Allowed to Overwrite Newer Agent Edits?</title>
<link>https://coderlegion.com/31834/should-an-ai-listing-draft-be-allowed-to-overwrite-newer-agent-edits</link>
<description>&lt;p&gt;Imagine a property agent starts an AI rewrite of a listing description. While it runs, a colleague corrects a factual detail in the same listing. The AI result then arrives, based on the older version.&lt;/p&gt;
&lt;p&gt;What should the application do with that result?&lt;/p&gt;
&lt;p&gt;A useful distinction is &lt;strong&gt;finishing generation&lt;/strong&gt; versus &lt;strong&gt;accepting a change&lt;/strong&gt;. The generated draft can be retained without replacing the current description.&lt;/p&gt;
&lt;p&gt;HTTP already has a building block for this problem. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.1&quot;&gt;RFC 9110, section 13.1.1&lt;/a&gt; describes &lt;code&gt;If-Match&lt;/code&gt; as a way to guard against accidental overwrites when clients update the same resource. With a supporting API, a client sends the strong ETag it read; a failed precondition can produce &lt;code&gt;412 Precondition Failed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a property-content editor, a possible design is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Record the listing revision used to generate the draft.&lt;/li&gt;
&lt;li&gt;Save the AI output as a separate proposal.&lt;/li&gt;
&lt;li&gt;Compare that base revision with the current listing when someone applies it.&lt;/li&gt;
&lt;li&gt;If they differ, show the original, current, and proposed text for review.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difficult part is the user experience. A changed punctuation mark and a corrected room count should not necessarily receive the same treatment. A version check catches a conflict; it does not decide whether the new wording is factually safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Would you block every stale AI draft, allow field-level merging, or always require a review screen? What would make your choice change?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is a hypothetical design question, not a claim about a particular product's implementation.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31834/should-an-ai-listing-draft-be-allowed-to-overwrite-newer-agent-edits</guid>
<pubDate>Tue, 20 Oct 2026 10:49:18 +0000</pubDate>
</item>
<item>
<title>Can a Keyboard-Only User Finish Your App’s Main Task?</title>
<link>https://coderlegion.com/31692/can-a-keyboard-only-user-finish-your-apps-main-task</link>
<description>&lt;p&gt;Pick the main task in your web app: submitting a form, uploading a file, booking an appointment, or creating a project.&lt;/p&gt;
&lt;p&gt;Now put the mouse aside and try to finish it using the keyboard.&lt;/p&gt;
&lt;p&gt;Where does the journey first become confusing or impossible?&lt;/p&gt;
&lt;p&gt;Some useful places to check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A custom dropdown that opens but cannot be operated&lt;/li&gt;
&lt;li&gt;A dialog that opens without a clear focus position&lt;/li&gt;
&lt;li&gt;A validation error that appears somewhere the user does not notice&lt;/li&gt;
&lt;li&gt;A drag-and-drop interaction with no usable alternative&lt;/li&gt;
&lt;li&gt;A focus indicator that disappears against the background&lt;/li&gt;
&lt;li&gt;A component you can enter but cannot leave&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;W3C’s &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html&quot;&gt;keyboard accessibility guidance&lt;/a&gt; explains why functionality needs a keyboard-operable route, with a limited exception for inherently path-dependent input. Its &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html&quot;&gt;no-keyboard-trap guidance&lt;/a&gt; addresses being able to leave a component again.&lt;/p&gt;
&lt;p&gt;A keyboard walkthrough is a useful first check. It does not establish full accessibility or replace screen-reader and other accessibility testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is one keyboard-accessibility bug your team found late, and what changed in your development or review process afterward?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you are building something new, which critical flow will you test first?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Waqas Ahmad | Software Engineer | Content Creator&lt;/em&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31692/can-a-keyboard-only-user-finish-your-apps-main-task</guid>
<pubDate>Mon, 19 Oct 2026 07:41:47 +0000</pubDate>
</item>
<item>
<title>Would an independent final-email rule check earn a place beside your existing tests?</title>
<link>https://coderlegion.com/31600/would-an-independent-final-email-rule-check-earn-a-place-beside-your-existing-tests</link>
<description>&lt;p&gt;I’m cun he, maker of InlinerAPI. If you maintain business receipts, reset messages or invitations, I’d appreciate one current-work comparison: &lt;a rel=&quot;nofollow&quot; href=&quot;https://inlinerapi.netlify.app/requirements&quot;&gt;https://inlinerapi.netlify.app/requirements&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Use synthetic final rendered HTML and an independently expected copy, displayed amount or CTA destination. The browser preview needs no signup. It checks the supplied content; it does not refresh application data, run your templates, validate tokens, or test inbox appearance or delivery.&lt;/p&gt;
&lt;p&gt;What rule did you attempt, what result did you observe, and how did setup and maintenance compare with your existing tests? If it helps recurring work, what would your team need to justify paying for these current checks?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31600/would-an-independent-final-email-rule-check-earn-a-place-beside-your-existing-tests</guid>
<pubDate>Sun, 18 Oct 2026 11:12:47 +0000</pubDate>
</item>
<item>
<title>Digital Masquerade: How Deepfakes Turn Social Engineering Into Fucking Hell</title>
<link>https://coderlegion.com/31507/digital-masquerade-how-deepfakes-turn-social-engineering-into-fucking-hell</link>
<description>&lt;p&gt;It starts with a message. The sender claims to be your company's CFO, and the subject is a &quot;confidential transaction.&quot; Confidential means &lt;em&gt;don't ask around&lt;/em&gt;. You feel the small, familiar itch of suspicion. It's an odd request. It smells a bit like phishing.&lt;/p&gt;
&lt;p&gt;So you do the sensible thing. You ask for a video call.&lt;/p&gt;
&lt;p&gt;The CFO is there. His face, his voice, the way he phrases things. A few colleagues you recognize are on the call too, nodding along. Everyone looks normal. Everyone sounds normal. Your suspicion quietly packs its bags and leaves, because you just did exactly what every security awareness slide tells you to do: you &lt;em&gt;verified with your own eyes&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Then you send the money.&lt;/p&gt;
&lt;p&gt;That isn't a hypothetical. In January 2024, a finance employee at the engineering firm Arup, working in Hong Kong, was reportedly walked through precisely this sequence. According to Hong Kong police, every other participant on that call was a deepfake. The employee made transfers totalling HK$200 million, about US$25.6 million. The fraud surfaced only when he later checked with the company's head office.&lt;/p&gt;
&lt;p&gt;His mistake wasn't carelessness. He did what a cautious person does. &lt;strong&gt;The entire security model of &quot;seeing is believing&quot; just got deprecated, and nobody pushed the changelog.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Faces and voices are now unauthenticated input. Detection tools and human intuition won't save you, because both are losing the arms race. The only defence that scales is Zero Trust applied to &lt;em&gt;people&lt;/em&gt;: bind high-risk actions to cryptographic identity and process, never to what someone looks or sounds like. A checklist, some pseudocode, and ready-to-use scripts are below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-seeing-is-believing-has-been-deprecated&quot;&gt;&quot;Seeing Is Believing&quot; Has Been Deprecated&lt;/h3&gt;
&lt;p&gt;The cost of faking a person used to be a film studio and a budget. Now it's footage that already exists online: conference talks, podcasts, earnings calls, that LinkedIn video where your VP says &quot;I'm thrilled to announce.&quot; Hong Kong police said the Arup attackers built their fakes from publicly available video and audio of online conferences and virtual meetings.&lt;/p&gt;
&lt;p&gt;The trend lines are ugly, with the usual caveat that this is an under-reported crime category:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TRM Labs' 2026 AI-in-Crime Adoption Index climbed to 54, up from 28 in 2024, and it reports that deepfake-scam losses in the first months of 2026 already exceeded the &lt;em&gt;entire&lt;/em&gt; 2025 total by 263%.&lt;/li&gt;
&lt;li&gt;The FBI's 2025 Internet Crime Report logged more than 22,000 AI-related fraud complaints with reported losses above $893 million, and congressional researchers have estimated that fewer than 5% of voice-clone victims report their losses at all.&lt;/li&gt;
&lt;li&gt;Deloitte has projected generative-AI-driven fraud losses in the US reaching US$40 billion by 2027, up from US$12.3 billion in 2023.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Treat any single number here as a rough indicator, not gospel. The direction is not in dispute.&lt;/p&gt;
&lt;p&gt;And it fits the wider picture from earlier in this series. Verizon's 2026 breach report found phone-centric lures succeeding about 40% more often than email in its simulation data, and Mandiant's M-Trends 2026 ranked vishing the second most common initial-access vector. Attackers have noticed that humans trust voices. Deepfakes simply make that trust cheap to counterfeit.&lt;/p&gt;
&lt;h3 id=&quot;the-anatomy-of-a-deepfake-heist-defender-s-edition-&quot;&gt;The Anatomy of a Deepfake Heist (Defender's Edition)&lt;/h3&gt;
&lt;p&gt;You can't defend against a chain you can't picture, so here's the structure. I'm keeping this at the level of &lt;em&gt;what to watch for&lt;/em&gt;, not how to build one.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reconnaissance.&lt;/strong&gt; Who approves payments, who reports to whom, and what does the target executive's public audio and video look like? Org charts and conference recordings are free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pretext.&lt;/strong&gt; Urgency, secrecy, and authority, delivered together. If you read the biological OPSEC post, you'll recognize the Cialdini toolkit. &quot;Confidential&quot; does double duty: it explains why you can't verify and it isolates you from colleagues who might.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Channel setup.&lt;/strong&gt; A spoofed email or message that books the call. In the Arup case, an initial message about a confidential transaction came first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The call.&lt;/strong&gt; Notice &lt;em&gt;multiple&lt;/em&gt; fake participants. That's social proof, manufactured. One suspicious face is a red flag; four familiar ones is consensus.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The ask.&lt;/strong&gt; A payment, a credential reset, a &quot;temporary&quot; access grant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The exit.&lt;/strong&gt; The call ends, the accounts go dark, and the money is already moving through several hops.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every step in that chain exploits a &lt;em&gt;human shortcut&lt;/em&gt;, not a software bug. That's the point of this series: you can't patch System 1.&lt;/p&gt;
&lt;h3 id=&quot;why-just-detect-the-deepfake-isn-t-a-plan&quot;&gt;Why &quot;Just Detect the Deepfake&quot; Isn't a Plan&lt;/h3&gt;
&lt;p&gt;Tempting idea: deploy a detector, flag fakes, done. Unfortunately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Humans are near coin-flip.&lt;/strong&gt; A 2024 meta-analysis of dozens of studies put average human detection accuracy for deepfakes at roughly 55%, barely better than guessing. Training helps a little, confidence helps not at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detectors degrade in the wild.&lt;/strong&gt; Video-conferencing compression smears exactly the artifacts detectors look for, real-time detection has latency budgets a live call doesn't care about, and models trained on last year's synthesis techniques miss this year's.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The attacker only needs one success.&lt;/strong&gt; A detector that's right 95% of the time is a 5% jackpot for someone running the attack at scale.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use detection as a signal. Never use it as a control. A control is something that still works when it's wrong.&lt;/p&gt;
&lt;h3 id=&quot;zero-trust-translated-for-humans&quot;&gt;Zero Trust, Translated for Humans&lt;/h3&gt;
&lt;p&gt;NIST's Zero Trust Architecture (SP 800-207) boils down to a principle you already know from network security: &lt;strong&gt;never trust, always verify&lt;/strong&gt;, and never grant trust based on where a request &lt;em&gt;appears&lt;/em&gt; to come from. We've applied that to packets and subnets for years. It's time to apply it to faces.&lt;/p&gt;
&lt;p&gt;The one-line version, which you're welcome to put on a mug:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Perception is not authentication.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A face on a screen and a voice on a line are &lt;em&gt;claims&lt;/em&gt;, the same as a username typed into a login form. You'd never treat a typed username as proof of identity. So stop treating a familiar voice as one.&lt;/p&gt;
&lt;p&gt;Here's the anti-pattern in code, because it's funnier when it compiles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# What the Arup-style attack exploits (pseudocode, do not ship)
def release_payment(call):
    if call.face_looks_like(&quot;CFO&quot;) and call.voice_sounds_like(&quot;CFO&quot;):
        wire(25_000_000)   # looks legit, ship it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the version that survives deepfakes, because it never asks the question &quot;does this look like the CFO?&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Zero Trust for payments (pseudocode)
def release_payment(req):
    assert req.exists_in_system_of_record()          # not just &quot;someone asked on a call&quot;
    assert len(req.approvers) &amp;gt;= 2                   # dual control
    assert all(a.passkey_verified for a in req.approvers)  # phishing-resistant auth
    assert req.beneficiary in vendor_master \
        or req.new_beneficiary_verified_via_callback()     # known number, not the caller's
    assert req.amount &amp;lt;= LIMIT or req.cooling_off_elapsed()
    wire(req)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing in the second version cares what anyone looks or sounds like. That's the design goal.&lt;/p&gt;
&lt;h3 id=&quot;the-verification-stack&quot;&gt;The Verification Stack&lt;/h3&gt;
&lt;p&gt;Think in layers. No single layer is bulletproof, but an attacker has to defeat &lt;em&gt;all&lt;/em&gt; of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Cryptographic identity for anything that moves money or grants access&lt;/strong&gt;&lt;br&gt;
Use phishing-resistant authentication (passkeys / FIDO2 hardware keys) for approvers and administrators, and require approvals to happen inside a system that records them, not in a chat or on a call. As covered in the identity-stack post, a hardware key is domain-bound and signs a challenge; a deepfake can imitate your CFO's face, but it can't produce her private key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Out-of-band callback, to a number &lt;em&gt;you&lt;/em&gt; already had&lt;/strong&gt;&lt;br&gt;
If a request arrives on one channel, confirm it on another, using contact details from your own directory. Not the number in the email. Not the number the caller offers. Not the meeting link they sent. The attacker controls everything they hand you; they don't control your HR system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Pre-agreed challenge phrases&lt;/strong&gt;&lt;br&gt;
Low-tech, effective, and underused. Agree on a secret with your leadership team and finance staff &lt;em&gt;in person or over an authenticated channel&lt;/em&gt;, rotate it, and make sure it's not derivable from public information. Add a duress variant (&quot;the code word that means someone is making me ask&quot;). Same trick works for families: voice-cloning scams that impersonate a relative in trouble are real, and a family code word costs nothing.&lt;/p&gt;
&lt;p&gt;One caution: a question about something &quot;only the real person would know&quot; works only if the answer isn't online. Ferrari reportedly foiled a fake-CEO call when an executive asked about a book the real CEO had recently recommended, and the call ended. That's a great story. It's also luck plus one sharp employee, so make it policy instead of hoping for sharp employees.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Process controls that make deepfakes irrelevant&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dual authorization&lt;/strong&gt; above a threshold, with approvers independent of each other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling-off periods&lt;/strong&gt; on large or unusual transfers. Urgency is the attacker's best friend; delay is your cheapest control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict handling of changes to payee bank details&lt;/strong&gt;, verified by callback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A written rule: no approvals by voice or video alone.&lt;/strong&gt; If the policy says it doesn't count, nobody can be pressured into counting it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. Meeting hygiene&lt;/strong&gt;&lt;br&gt;
Require authenticated participants (SSO-backed accounts), use a waiting room for anyone external, and treat an unfamiliar account or domain as a red flag no matter how convincing the face. &quot;Camera on&quot; is not proof of anything anymore.&lt;/p&gt;
&lt;p&gt;A note on &quot;liveness stunts&quot; like asking someone to turn their head or wave a hand in front of their face: these can trip up weaker real-time fakes, and they're fine as an extra speed bump. They're also getting less reliable every quarter. Don't build a control on them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. The help desk and hiring pipelines&lt;/strong&gt;&lt;br&gt;
Attackers go where the verification is weakest. Reportedly, the 2023 MGM Resorts intrusion started with a phone call to an IT help desk. Never reset credentials or MFA enrolment on the strength of a voice alone: verify through an authenticated channel, a manager approval in a system of record, or a stronger identity check. The same logic applies to remote hiring, where a polished candidate on camera is now a claim to be verified, not a fact.&lt;/p&gt;
&lt;h3 id=&quot;make-it-socially-safe-to-say-i-ll-call-you-back-&quot;&gt;Make It Socially Safe to Say &quot;I'll Call You Back&quot;&lt;/h3&gt;
&lt;p&gt;Here's the cultural bug that kills most of this. Authority bias is powerful. An employee who suspects something still has to be willing to say &quot;I'm going to verify this&quot; to someone who outranks them, on a call, while the supposed CFO says it's urgent.&lt;/p&gt;
&lt;p&gt;Give people the words, and make sure leadership has publicly agreed to be on the receiving end of them:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Our policy requires me to confirm this through the payments system. It'll take five minutes.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I'm going to call you back on the number in the directory. If this is real, nothing changes.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I can't approve anything on a call, and neither can you. Please submit it in the system.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A real executive will say &quot;good, that's exactly what I want you to do.&quot; A fake one will get impatient. &lt;strong&gt;Impatience is data.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-about-reducing-the-source-material-&quot;&gt;What About Reducing the Source Material?&lt;/h3&gt;
&lt;p&gt;Executives whose voice and video are all over the internet are the easiest to impersonate, and trimming that footprint is reasonable hygiene. Don't confuse it with security. You can't un-publish ten years of conference talks, and the attacker only needs a few seconds of clean audio. Assume you &lt;em&gt;can&lt;/em&gt; be faked, and design as if everyone can be.&lt;/p&gt;
&lt;p&gt;The same applies to provenance standards like content credentials for published media: genuinely useful for establishing where a clip came from, but they don't help on a live call where nothing was published in advance.&lt;/p&gt;
&lt;h3 id=&quot;the-checklist&quot;&gt;The Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;High-risk approvals happen &lt;strong&gt;in a system of record&lt;/strong&gt;, never on a call or in a chat&lt;/li&gt;
&lt;li&gt;Approvers use &lt;strong&gt;phishing-resistant authentication&lt;/strong&gt; (passkeys / hardware keys)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dual authorization&lt;/strong&gt; and a &lt;strong&gt;cooling-off period&lt;/strong&gt; above a defined threshold&lt;/li&gt;
&lt;li&gt;Bank-detail changes verified by &lt;strong&gt;callback to a number from your own directory&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A rotated &lt;strong&gt;challenge phrase&lt;/strong&gt; (with a duress variant) for leadership and finance&lt;/li&gt;
&lt;li&gt;A written rule: &lt;strong&gt;voice or video alone never counts as approval&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Meetings require &lt;strong&gt;authenticated participants&lt;/strong&gt;, with a lobby for externals&lt;/li&gt;
&lt;li&gt;Help desk can't reset credentials or MFA on a &lt;strong&gt;voice alone&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Staff have &lt;strong&gt;scripts&lt;/strong&gt; and leadership has agreed to honour them&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;family code word&lt;/strong&gt; at home, because your relatives have voices too&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-point&quot;&gt;The Point&lt;/h3&gt;
&lt;p&gt;For about a century, &quot;I saw him with my own eyes&quot; and &quot;I'd know that voice anywhere&quot; were gold-standard evidence. They're now claims at best. That's uncomfortable, and it's also clarifying, because it forces us to do what security engineers have been saying all along: stop trusting appearance and start trusting verification.&lt;/p&gt;
&lt;p&gt;In my own recruiter-phishing story, the tell wasn't how convincing the persona was. It was the &lt;em&gt;channel&lt;/em&gt; the attacker wanted me to use. Deepfakes are the same lesson, upgraded: the more convincing the face, the more your attention belongs on the process behind it.&lt;/p&gt;
&lt;p&gt;Trust is a budget. Spend it only on things that can sign.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; What's the verification step in your company for an urgent payment request, and would it survive a convincing deepfake of your CEO? Drop it in the comments. If the honest answer is &quot;we'd probably just send it,&quot; that's the most useful thing you can learn today.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31507/digital-masquerade-how-deepfakes-turn-social-engineering-into-fucking-hell</guid>
<pubDate>Sat, 17 Oct 2026 10:24:11 +0000</pubDate>
</item>
<item>
<title>Interconnected Growth: A Community Begins with Each of Us</title>
<link>https://coderlegion.com/31444/interconnected-growth-a-community-begins-with-each-of-us</link>
<description>&lt;p&gt;There is a quiet vulnerability in sharing our work. Whether we publish an article, offer an idea, or describe something we have created, we put a small part of ourselves in front of others. Behind that contribution may be hours of effort, years of experience, or the courage to be visible for the first time.&lt;/p&gt;
&lt;p&gt;Naturally, we hope someone will respond. A thoughtful comment, a reaction, or a simple acknowledgment can reassure us that our contribution has reached another person.&lt;/p&gt;
&lt;p&gt;Within a growing community, however, that hope carries a shared responsibility. If we all publish and then wait for others to begin the conversation, even a website filled with valuable contributions can feel remarkably quiet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Participation Makes Belonging Possible&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A developing platform needs more than a steady flow of new content. It needs people who take an interest in what others bring to it.&lt;/p&gt;
&lt;p&gt;It is understandable to focus on our own work. Time is limited, responsibilities compete for our attention, and many of us are trying to build something meaningful alongside other commitments. Still, when we hope for engagement, it is worth considering whether we are making room to offer it ourselves.&lt;/p&gt;
&lt;p&gt;Reading another person’s article, acknowledging a useful observation, or asking a sincere question helps establish the culture we would like to experience. These small actions make participation feel worthwhile, particularly for people who are still finding their confidence.&lt;/p&gt;
&lt;p&gt;For someone wondering whether their work belongs, a few considerate words may carry more weight than we realize.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Mutual Support Without Keeping Score&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Supporting one another should never become an obligation to exchange praise. A reaction does not create a debt, and leaving a comment should not come with an expectation of repayment.&lt;/p&gt;
&lt;p&gt;The value lies in sincere attention.&lt;/p&gt;
&lt;p&gt;We can recognize effort without agreeing with every conclusion. We can offer a different perspective respectfully, share an experience that adds to the discussion, or explain what we found useful. Meaningful engagement gives someone evidence that their contribution has been considered.&lt;/p&gt;
&lt;p&gt;Over time, that attention builds familiarity and trust. People become more comfortable sharing unfinished ideas, asking questions, and offering their own knowledge. The community gains depth because its members feel there is someone on the other side of the conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Our Growth Is Connected&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On a website that is still developing its community, each interaction helps shape what the platform becomes.&lt;/p&gt;
&lt;p&gt;When we respond thoughtfully to someone’s work, we give them a reason to return and contribute again. Their continued participation may help another person learn, connect, or feel encouraged. What begins as a small gesture can gradually strengthen the experience for everyone.&lt;/p&gt;
&lt;p&gt;No individual can create an active community alone, and nobody should feel responsible for responding to everything. But we can each contribute to an environment where attention moves in more than one direction.&lt;/p&gt;
&lt;p&gt;Before leaving after our next post, perhaps we can spend a little time with someone else’s. Read beyond the headline. Acknowledge an idea that deserves consideration. Leave the kind of thoughtful response we would appreciate receiving.&lt;/p&gt;
&lt;p&gt;Every contribution represents a person hoping to be heard. A growing community becomes stronger when we make space to hear one another.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31444/interconnected-growth-a-community-begins-with-each-of-us</guid>
<pubDate>Fri, 16 Oct 2026 19:17:32 +0000</pubDate>
</item>
<item>
<title>What habits daily make you a really great software engineer?</title>
<link>https://coderlegion.com/31423/what-habits-daily-make-you-a-really-great-software-engineer</link>
<description>&lt;p&gt;Well, what do you think what are some top notch habits that makes you in top zone of engineering&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31423/what-habits-daily-make-you-a-really-great-software-engineer</guid>
<pubDate>Fri, 16 Oct 2026 17:09:05 +0000</pubDate>
</item>
<item>
<title>Good debugging starts with better questions</title>
<link>https://coderlegion.com/31271/good-debugging-starts-with-better-questions</link>
<description>&lt;p&gt;When production breaks, the first question should not be: &lt;strong&gt;Who broke it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A better starting point is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;What changed in the system?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That shifts the discussion toward evidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;recent deployments&lt;/li&gt;
&lt;li&gt;configuration changes&lt;/li&gt;
&lt;li&gt;dependency behavior&lt;/li&gt;
&lt;li&gt;traffic patterns&lt;/li&gt;
&lt;li&gt;logs, metrics, and traces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me, evidence-first debugging usually leads to faster diagnosis and better team behavior.&lt;/p&gt;
&lt;p&gt;What question do you ask first when a production issue starts?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31271/good-debugging-starts-with-better-questions</guid>
<pubDate>Fri, 16 Oct 2026 03:27:21 +0000</pubDate>
</item>
<item>
<title>Discussion: What does it take to make an MCP server production-ready?</title>
<link>https://coderlegion.com/31078/discussion-what-does-it-take-to-make-an-mcp-server-production-ready</link>
<description>&lt;p&gt;A local MCP server can be straightforward to run. Production deployments introduce different requirements when agents interact with remote APIs, databases, and execution environments.&lt;/p&gt;
&lt;p&gt;Our new SEOSiri guide examines several important areas:&lt;/p&gt;
&lt;p&gt;Scoped authentication and authorization&lt;/p&gt;
&lt;p&gt;Secure SSE-based remote communication&lt;/p&gt;
&lt;p&gt;Stateless service design&lt;/p&gt;
&lt;p&gt;Response pruning and pagination for token efficiency&lt;/p&gt;
&lt;p&gt;Sandboxed execution for dynamic code&lt;/p&gt;
&lt;p&gt;The central engineering question: how do you give an AI agent useful capabilities without granting unnecessary access or creating avoidable operational risk?&lt;/p&gt;
&lt;p&gt;Read the technical breakdown:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/10/building-mcp-servers-production-security.html&quot;&gt;https://www.seosiri.com/2026/10/building-mcp-servers-production-security.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For developers working with MCP, AI infrastructure, and DevSecOps.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31078/discussion-what-does-it-take-to-make-an-mcp-server-production-ready</guid>
<pubDate>Wed, 14 Oct 2026 14:33:00 +0000</pubDate>
</item>
<item>
<title>Why Does Code Work Locally but Fail in Production?</title>
<link>https://coderlegion.com/31069/why-does-code-work-locally-but-fail-in-production</link>
<description>&lt;p&gt;One of the most frustrating problems in software development is when everything works perfectly on your machine, but breaks after deployment. 💀&lt;/p&gt;
&lt;p&gt;The tricky part? The code itself isn't always the problem.&lt;/p&gt;
&lt;p&gt;Here are five things I would investigate first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Environment variables&lt;br&gt;
A missing API key, incorrect URL, or improperly configured environment variable can break an otherwise working application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dependency differences&lt;br&gt;
Different package versions or runtime environments can produce unexpected behavior.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API and CORS configuration&lt;br&gt;
An endpoint that works locally might fail in production because of origin restrictions, incorrect URLs, or deployment configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database permissions&lt;br&gt;
A feature may work with local development permissions but fail when production security policies are enforced.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build and deployment settings&lt;br&gt;
Incorrect routes, missing assets, case-sensitive file paths, and build configuration differences can cause issues that never appeared locally.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My rule is simple: don't assume the deployment is broken until you've identified where the failure actually occurs.&lt;/p&gt;
&lt;p&gt;Reproduce the issue, inspect the logs, isolate the failing layer, and test the fix before deploying again.&lt;/p&gt;
&lt;p&gt;💬 Question for developers: What's the most confusing production bug you've encountered, and what turned out to be the actual cause?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31069/why-does-code-work-locally-but-fail-in-production</guid>
<pubDate>Wed, 14 Oct 2026 12:39:53 +0000</pubDate>
</item>
<item>
<title>Where do squared error and cross-entropy actually come from?</title>
<link>https://coderlegion.com/30983/where-do-squared-error-and-cross-entropy-actually-come-from</link>
<description>&lt;p&gt;&lt;strong&gt;Short answer: maximum likelihood. Choose a distribution for the output, then:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read its parameter as the network's output → output function&lt;/li&gt;
&lt;li&gt;Take −log of it → error function&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Gaussian (regression)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Output: ŷ = Wᵀh + b (the mean)&lt;/li&gt;
&lt;li&gt;Error: −log N(y; ŷ, σ²) → squared error (constants drop out)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bernoulli (binary classification)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Output: exponentiate + normalize → ŷ = σ(z)&lt;/li&gt;
&lt;li&gt;Error: −log P(y) → cross-entropy = softplus((1 − 2y)z)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why the pairing matters: with cross-entropy, the log cancels the sigmoid's exponential, so ∂J/∂z = σ(z) − y. With squared error, σ′(z) stays in the gradient and goes to ~0 when the model is confidently wrong, so learning stalls.&lt;/p&gt;
&lt;p&gt;Full derivations: &lt;a rel=&quot;nofollow&quot; href=&quot;https://husseinmahdi.xyz/writing/series/math-behind-neural-networks/&quot;&gt;https://husseinmahdi.xyz/writing/series/math-behind-neural-networks/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Next up: softmax + cross-entropy for k classes.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30983/where-do-squared-error-and-cross-entropy-actually-come-from</guid>
<pubDate>Wed, 14 Oct 2026 05:20:00 +0000</pubDate>
</item>
<item>
<title>Stop setting your dark mode background to #000000 and text to #FFFFFF</title>
<link>https://coderlegion.com/30736/stop-setting-your-dark-mode-background-to-000000-and-text-to-ffffff</link>
<description>&lt;p&gt;Pure black on pure white creates high chromatic contrast that causes &lt;em&gt;eye strain&lt;/em&gt; and &lt;em&gt;severe halation&lt;/em&gt; for readers with astigmatism.&lt;/p&gt;
&lt;p&gt;A proper dark theme uses layered &lt;strong&gt;dark grays/slates (#121212, #1E1E1E)&lt;/strong&gt; to express depth through elevation shadows and tinted surfaces rather than stark, blinding contrast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are your favorite color palettes for comfortable dark mode reading?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30736/stop-setting-your-dark-mode-background-to-000000-and-text-to-ffffff</guid>
<pubDate>Sun, 11 Oct 2026 18:40:37 +0000</pubDate>
</item>
<item>
<title>Would you use free assets in your games?</title>
<link>https://coderlegion.com/30611/would-you-use-free-assets-in-your-games</link>
<description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;I would, and I don’t think there’s any shame in it.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’ve never really understood the stigma around using free assets in game development. Sometimes, people talk about them as if downloading a free model or sound somehow makes the rest of your work less valuable.&lt;/p&gt;
&lt;p&gt;But why?&lt;/p&gt;
&lt;p&gt;For anyone unfamiliar with the term, assets are the individual things that go into a game: trees, buildings, characters, sounds, animations, textures, and so on. Making all of those takes time, and creating a game involves much more than making its individual parts.&lt;/p&gt;
&lt;p&gt;If someone offers a useful asset for free and allows others to use it, I see that as an opportunity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Free doesn’t mean bad.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The price of an asset doesn’t tell you whether it looks good, works properly, or fits your game. A paid model can be unsuitable for your project, while a free one can be exactly what you need.&lt;/p&gt;
&lt;p&gt;Some creators choose to share their work because they want to help other developers. Kenney, for example, makes its game assets available under CC0, allowing people to use them without requiring credit. These resources are offered for a reason: so people can build with them.&lt;/p&gt;
&lt;p&gt;There’s nothing embarrassing about accepting that help.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Your time matters, too.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine you need a simple chair for a room in your game. You could spend days learning how to model it, give it the right surface details, and get it working in the engine. Or you could find a suitable free chair and spend that time improving something the player will actually do.&lt;/p&gt;
&lt;p&gt;Neither choice is automatically wrong. If you want to learn how to make furniture, creating the chair yourself makes sense. If your goal is to finish a playable game, using an existing one might make more sense.&lt;/p&gt;
&lt;p&gt;Personally, I don’t think developers should feel pressured to spend days proving they can make an ordinary chair when their game needs attention elsewhere.&lt;/p&gt;
&lt;p&gt;That time could go towards a better mission, smoother movement, clearer menus, or fixing an annoying bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;A downloaded asset doesn’t build the game for you.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is probably the part that gets overlooked most often.&lt;/p&gt;
&lt;p&gt;Downloading a door doesn’t make it open. You still need to decide how the player interacts with it, whether it needs a key, what happens when it’s locked, and how it connects to the rest of the game.&lt;/p&gt;
&lt;p&gt;Downloading trees doesn’t create an interesting forest. You still have to build the space, create paths, place things thoughtfully, adjust the lighting, and make sure the game runs well.&lt;/p&gt;
&lt;p&gt;The person who made the asset did valuable work. The person who turns it into part of a playable experience also does valuable work.&lt;/p&gt;
&lt;p&gt;Using someone else’s model doesn’t erase everything you built around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can use the same pieces and create something different.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Two people can cook with the same ingredients and make very different meals. I think game assets work in a similar way.&lt;/p&gt;
&lt;p&gt;The same house model could become a peaceful home in one game and an abandoned building in another. Different lighting, sounds, surroundings, and events can completely change how that place feels.&lt;/p&gt;
&lt;p&gt;Custom artwork can certainly help a game stand out. But a game’s identity also comes from its ideas, its atmosphere, its story, and the way it plays.&lt;/p&gt;
&lt;p&gt;Every rock and table doesn’t need to be exclusive for the overall experience to feel personal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Free assets help you find out whether an idea actually works.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An idea can sound amazing when you write it down and feel completely different once you try to play it.&lt;/p&gt;
&lt;p&gt;Having models, sounds, and animations available lets you reach that playable stage sooner. You can test a mechanic, try a different layout, or discover that something needs changing before spending months making everything yourself.&lt;/p&gt;
&lt;p&gt;And if a free asset still works well in the finished game, why replace it purely because it was free?&lt;/p&gt;
&lt;p&gt;I’d rather replace something because it doesn’t fit or causes problems, not because I feel guilty about how much it cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Not everyone has a studio’s budget.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A solo developer might be handling the gameplay, environments, menus, sound, testing, and writing. That’s already a lot for one person.&lt;/p&gt;
&lt;p&gt;They may not have the money to hire people for every task, or the years needed to become good at every part of development.&lt;/p&gt;
&lt;p&gt;Free assets make it easier for people to learn and give their ideas a real chance. They can also leave room in the budget for things that need special attention: perhaps an original main character, music, or an important location.&lt;/p&gt;
&lt;p&gt;Using free resources where they fit can help you afford custom work where it matters most.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Of course, you still need to choose carefully.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’m not saying every free asset belongs in every game.&lt;/p&gt;
&lt;p&gt;Some won’t match the look you’re going for. Some might slow the game down or take too much effort to adjust. You also need to check what the creator allows you to do with them and give credit when required.&lt;/p&gt;
&lt;p&gt;And you should be honest about your contribution. Using someone’s model is fine when you have permission. Claiming you made that model yourself is a different issue.&lt;/p&gt;
&lt;p&gt;Those are fair things to discuss. But none of them makes using free assets shameful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Judge the result.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If a game looks messy, runs badly, or isn’t enjoyable, those are fair criticisms. Explain what isn’t working.&lt;/p&gt;
&lt;p&gt;Simply saying “that’s a free asset” doesn’t tell me what’s wrong with the game. Paying for the same object wouldn’t suddenly make the gameplay better.&lt;/p&gt;
&lt;p&gt;For me, the question is simple: does this asset fit what I’m making, and does using it help me move the project forward?&lt;/p&gt;
&lt;p&gt;If the answer is yes, I’m happy to use it.&lt;/p&gt;
&lt;p&gt;I respect people who create everything themselves. I also respect people who know where to spend their time and use the resources available to them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;So yes, I would use free assets in my game. Why should a useful resource become something to be ashamed of just because it didn’t cost money?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30611/would-you-use-free-assets-in-your-games</guid>
<pubDate>Sat, 10 Oct 2026 14:55:56 +0000</pubDate>
</item>
<item>
<title>The rendering of markdown for code block is not working well in CoderLegion?</title>
<link>https://coderlegion.com/30576/the-rendering-of-markdown-for-code-block-is-not-working-well-in-coderlegion</link>
<description>&lt;p&gt;For example:&lt;/p&gt;
&lt;h4 id=&quot;angular-example&quot;&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/&quot;&gt;Angular Example&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/main.ts&quot;&gt;main.ts&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ThemeLoader.init();
bootstrapApplication(AppComponent, appConfig); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/zijianhuang/DemoCoreWeb/blob/master/AngularHeroes/src/app/theme-select.component.ts&quot;&gt;theme-select.component.ts&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;constructor() {
this.themes = ThemeConfigConstants.themesDic ? Object.keys(ThemeConfigConstants.themesDic).map(k =&amp;gt; {
const c = ThemeConfigConstants.themesDic![k];
const obj: ThemeDef = {
display: c.display,
filePath: k,
dark: c.dark
};
return obj;
}) : undefined;
}

themeSelectionChang(e: MatSelectChange) {
ThemeLoader.loadTheme(e.value);
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30576/the-rendering-of-markdown-for-code-block-is-not-working-well-in-coderlegion</guid>
<pubDate>Sat, 10 Oct 2026 10:08:04 +0000</pubDate>
</item>
<item>
<title>I turned strings into letter arrays and started rebuilding URLs with index ranges</title>
<link>https://coderlegion.com/30523/i-turned-strings-into-letter-arrays-and-started-rebuilding-urls-with-index-ranges</link>
<description>&lt;p&gt;I was just playing with a tiny idea:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What if every string was just an array of single characters… and I could pull pieces out by index ranges to form words, paths, or API links?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That experiment became &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/string-range-tokens&quot;&gt;string-range-tokens&lt;/a&gt;&lt;/strong&gt; — a zero-dependency ESM package that does exactly that.&lt;/p&gt;
&lt;h3 id=&quot;the-core-idea&quot;&gt;The core idea&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { toTokens, sub, build } from 'string-range-tokens';

const t = toTokens('https://api.example.com/v1/users');

sub(t, 0, 4);                 // &quot;https&quot;
sub(t, 23, 25, [26, 31]);     // &quot;/v1/users&quot;
sub(t, -5, -1);               // &quot;users&quot;   ← negative indexes work
build(t, [0, 4], [23, 25]);   // &quot;https/v1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;toTokens&lt;/code&gt; turns any string into a proper array of characters (Unicode-safe).&lt;br&gt;
&lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt; let you reassemble any combination of inclusive ranges.&lt;/p&gt;
&lt;h3 id=&quot;why-bother-&quot;&gt;Why bother?&lt;/h3&gt;
&lt;p&gt;Sometimes you want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assemble API paths or query fragments from a shared token pool&lt;/li&gt;
&lt;li&gt;Avoid writing the full string literal in one place&lt;/li&gt;
&lt;li&gt;Pick substrings by position without regex&lt;/li&gt;
&lt;li&gt;Build tiny string “templates” from a fixed alphabet&lt;/li&gt;
&lt;li&gt;Teach indexing, ranges, and a bit of recursion in a playful way&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not encryption (never hide real secrets with it). It’s just a clean, lightweight way to work with strings as letter arrays.&lt;/p&gt;
&lt;h3 id=&quot;extra-powers&quot;&gt;Extra powers&lt;/h3&gt;
&lt;h4 id=&quot;plan-find-the-ranges-that-rebuild-a-target&quot;&gt;&lt;code&gt;plan&lt;/code&gt; – find the ranges that rebuild a target&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const pool = toTokens('the quick brown fox');
const ranges = plan(pool, 'brew');
build(pool, ...ranges); // &quot;brew&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;scatter-gather-seeded-shuffle-reconstruct&quot;&gt;&lt;code&gt;scatter&lt;/code&gt; / &lt;code&gt;gather&lt;/code&gt; – seeded shuffle + reconstruct&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { pool, key } = scatter('/api/orders', 7);
gather(pool, key); // &quot;/api/orders&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful for deterministic scrambling when you want both the shuffled tokens and a way to put them back together.&lt;/p&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i string-range-tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or just copy the source — it’s a single file with no dependencies.&lt;/p&gt;
&lt;p&gt;Works in Node 18+, Deno, Bun, and the browser.&lt;br&gt;
Both JavaScript and TypeScript implementations are included.&lt;/p&gt;
&lt;p&gt;CDN:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { toTokens, sub, build, plan, scatter, gather } 
  from 'https://cdn.jsdelivr.net/npm/string-range-tokens@1.0.0/src/index.js';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/string-range-tokens&quot;&gt;string-range-tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/srTokens&quot;&gt;Figsh/srTokens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;If you end up using it for something fun (or weird), I’d love to hear about it.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30523/i-turned-strings-into-letter-arrays-and-started-rebuilding-urls-with-index-ranges</guid>
<pubDate>Fri, 09 Oct 2026 22:09:28 +0000</pubDate>
</item>
<item>
<title>I Taught AI to Review My Code So I Never Miss Leg Day Again</title>
<link>https://coderlegion.com/30447/i-taught-ai-to-review-my-code-so-i-never-miss-leg-day-again</link>
<description>&lt;p&gt;It's 10:48 p.m. on a Sunday. I'm folded over my desk like a question mark, reviewing a 1,400-line pull request titled &quot;small cleanup.&quot;&lt;/p&gt;
&lt;p&gt;My neck has developed opinions. My quads have filed a bug report titled &lt;em&gt;&quot;Not used since the last release.&quot;&lt;/em&gt; Status: won't fix.&lt;/p&gt;
&lt;p&gt;Somewhere in this diff, around line 812, there's a bug. I know it's there the way you know there's a spider in the room. I just can't see it, because I've been reading for ninety minutes and my eyes have quietly switched from &lt;em&gt;reading&lt;/em&gt; to &lt;em&gt;scrolling with intent&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;That was the night I made a rule: &lt;strong&gt;the code works for me, or it doesn't work here.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Your evenings aren't disappearing because you're slow. They're disappearing because AI made code cheap and review expensive. The fix is a four-layer pipeline where machines reject what machines can reject, an AI does the first pass, and you only spend human attention on the stuff that deserves it. Config snippets and a &quot;steal this&quot; checklist are at the bottom.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;first-an-honest-word-about-the-title&quot;&gt;First, an Honest Word About the Title&lt;/h3&gt;
&lt;p&gt;Yes, I close pull requests in five seconds. Yes, I have done it from a plank. GitHub Mobile, shaking arms, steady conscience.&lt;/p&gt;
&lt;p&gt;But I don't do it because a robot typed &quot;LGTM.&quot; I do it because the previous four hours and fifty-nine minutes of review &lt;strong&gt;already happened&lt;/strong&gt;, automatically, before the pull request ever reached my phone.&lt;/p&gt;
&lt;p&gt;The five-second merge is the &lt;em&gt;output&lt;/em&gt; of a system, not a leap of faith. A leap of faith is how you end up writing a postmortem.&lt;/p&gt;
&lt;h3 id=&quot;why-your-evenings-are-disappearing-it-s-the-volume-not-you-&quot;&gt;Why Your Evenings Are Disappearing (It's the Volume, Not You)&lt;/h3&gt;
&lt;p&gt;Here's the part nobody put in the AI-coding marketing deck. Faros AI analyzed telemetry from more than 10,000 developers in 2025, and teams with high AI adoption completed about 21% more tasks and merged about 98% more pull requests. Great. But pull requests also got roughly 154% larger, and &lt;strong&gt;median review time went up about 91%&lt;/strong&gt;, while organization-level delivery metrics stayed essentially flat.&lt;/p&gt;
&lt;p&gt;DORA's 2025 research landed in the same neighbourhood. Roughly 90% of respondents now use AI at work, AI adoption is finally associated with higher throughput, and it's &lt;em&gt;still&lt;/em&gt; associated with worse delivery stability. DORA's framing, which I can't improve on: AI is an &lt;strong&gt;amplifier&lt;/strong&gt;. It amplifies a healthy delivery system and it amplifies a chaotic one, with equal enthusiasm.&lt;/p&gt;
&lt;p&gt;Translation: the bottleneck moved. It used to be writing code. Now it's &lt;em&gt;you&lt;/em&gt;, at 11 p.m., reading code that nobody typed. Congratulations, you're the world's most expensive linter.&lt;/p&gt;
&lt;p&gt;So let's stop being one.&lt;/p&gt;
&lt;h3 id=&quot;the-four-layers-a-k-a-how-i-get-to-leg-day-&quot;&gt;The Four Layers (a.k.a. How I Get to Leg Day)&lt;/h3&gt;
&lt;h4 id=&quot;layer-0-don-t-review-what-a-machine-can-reject&quot;&gt;Layer 0: Don't Review What a Machine Can Reject&lt;/h4&gt;
&lt;p&gt;The single biggest waste of human review time is arguing about things a tool can settle in four seconds: formatting, unused imports, type errors, a failing test, a leaked secret, a dependency with a known CVE. If a human has ever typed &quot;nit: missing newline,&quot; your pipeline is broken, not your colleague.&lt;/p&gt;
&lt;p&gt;Make all of it a &lt;strong&gt;required check&lt;/strong&gt; that blocks the merge button. Here's a minimal, boring example for a Go project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: ci
on: [pull_request]

permissions:
  contents: read

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4   # pin to a full commit SHA in real life
      - uses: actions/setup-go@v5   # same here
        with:
          go-version: stable
      - run: test -z &quot;$(gofmt -l .)&quot;
      - run: go vet ./...
      - run: go test -race ./...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;About that pinning comment: if you read my supply-chain post, you know why. Tags are mutable, commit SHAs are not. Your CI pipeline is the most privileged thing in your repo, so treat its dependencies accordingly. Add a secret scanner and a dependency audit to the same job and you've deleted roughly half of what used to eat your Sunday night.&lt;/p&gt;
&lt;h4 id=&quot;layer-1-the-ai-first-pass-your-tireless-slightly-overconfident-junior-&quot;&gt;Layer 1: The AI First Pass (Your Tireless, Slightly Overconfident Junior)&lt;/h4&gt;
&lt;p&gt;This is where an AI reviewer earns its keep: summarizing what a 1,400-line diff actually does, catching the boring-but-real stuff (ignored error returns, missing nil checks, tests that don't test), and giving you a map before you enter the cave.&lt;/p&gt;
&lt;p&gt;Rules I follow so it stays useful instead of noisy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tell it what you care about, in writing.&lt;/strong&gt; Several AI reviewers read repo-level instruction files such as &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; and treat them as review criteria. Check your tool's docs for the exact filename. Mine looks like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Review rules
- Flag any new heap allocation in files under /hotpath.
- Flag any syscall wrapper that ignores its error return.
- Flag changes under /crypto that don't touch a test file.
- Do not comment on formatting. CI already handles that.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expect false positives.&lt;/strong&gt; Industry estimates for typical tools tend to land somewhere around 5 to 15%, and a tool that cries wolf gets muted within a week. Muted tools protect nobody.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know the meter is running.&lt;/strong&gt; Since GitHub's billing change this June, each Copilot review consumes both AI credits and Actions minutes. &quot;Free robot reviewer&quot; now comes with a taxi meter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run the Leg Day Test before you trust anything.&lt;/strong&gt; Take a representative PR, seed it with four bugs you know about, and open a second, &lt;em&gt;correct&lt;/em&gt; PR to see whether the tool invents a problem that isn't there. Count real catches and false alarms separately. Re-run it every few months, because vendors swap the underlying models and the reviewer you tested in spring is not necessarily the one running in autumn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AI is a junior who never sleeps. It is not the person who signs off.&lt;/p&gt;
&lt;h4 id=&quot;layer-2-humans-review-intent-not-syntax&quot;&gt;Layer 2: Humans Review Intent, Not Syntax&lt;/h4&gt;
&lt;p&gt;With layers 0 and 1 done, your job shrinks to the part machines are genuinely bad at. Four questions, in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Should this exist?&lt;/strong&gt; Is this the right change for the actual problem?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What breaks at 3 a.m.?&lt;/strong&gt; Failure modes, rollbacks, observability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's the blast radius?&lt;/strong&gt; One endpoint, or the whole auth flow?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What would an attacker do with this?&lt;/strong&gt; (If you're a regular here, you already ask this one by reflex.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keep the diff small, because attention has a shelf life. The classic SmartBear/Cisco study of roughly 2,500 reviews found defect detection falls off sharply beyond a few hundred lines and after about an hour of continuous reviewing. A 1,400-line &quot;small cleanup&quot; isn't reviewable. It's a hostage situation. Send it back and ask for three PRs.&lt;/p&gt;
&lt;h4 id=&quot;layer-3-merge-policy-a-k-a-where-the-five-second-merge-becomes-legal&quot;&gt;Layer 3: Merge Policy, a.k.a. Where the Five-Second Merge Becomes Legal&lt;/h4&gt;
&lt;p&gt;This is the layer that makes plank-merging safe instead of reckless:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .github/CODEOWNERS
*            @you
/auth/       @you @second-pair-of-eyes
/crypto/     @you @second-pair-of-eyes
/.github/    @you @second-pair-of-eyes
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Branch protection with required checks.&lt;/strong&gt; Nothing merges red. No exceptions for &quot;it's just a typo,&quot; because the typo is always in the config file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mandatory second human on risky paths:&lt;/strong&gt; auth, crypto, payments, infrastructure, the CI config itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-merge only for low-risk changes with green checks&lt;/strong&gt;: docs, patch-level dependency bumps, generated files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No special trust for AI-authored code.&lt;/strong&gt; If anything it gets &lt;em&gt;more&lt;/em&gt; scrutiny, since nobody on the team can say &quot;I wrote it, I remember why.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the five-second approval isn't a gamble. It's a rubber stamp on a document that's already been notarized three times.&lt;/p&gt;
&lt;h3 id=&quot;discipline-is-a-pipeline&quot;&gt;Discipline Is a Pipeline&lt;/h3&gt;
&lt;p&gt;Here's the philosophical bit, and then I'll let you go.&lt;/p&gt;
&lt;p&gt;Willpower is a terrible architecture. It's a flaky test: passes when you're rested, fails at 11 p.m. on a Sunday, and nobody can reproduce it. Psychologists have studied this for decades. Precommitment and &quot;if X, then Y&quot; implementation plans beat raw motivation because they move the decision out of the moment where you're weakest.&lt;/p&gt;
&lt;p&gt;A review pipeline is precommitment for your career. You decided once, in daylight, what &quot;good enough to merge&quot; means. After that, the system enforces it while you're tired, rushed, or hanging off a pull-up bar.&lt;/p&gt;
&lt;p&gt;The gym works the same way. You don't &quot;feel like&quot; brushing your teeth every morning; it's a routine. Leg day is a CI job for your body: scheduled, boring, and fails visibly when you skip it. Last post I said sleep is a build step. This one's the sequel: &lt;strong&gt;the gym is your deploy window.&lt;/strong&gt; Both are non-negotiable, and both die first when the code starts running you.&lt;/p&gt;
&lt;h3 id=&quot;where-this-goes-wrong-because-something-always-does-&quot;&gt;Where This Goes Wrong (Because Something Always Does)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rubber-stamping.&lt;/strong&gt; If the AI reviewer says &quot;looks good&quot; and you stop reading, you haven't automated review, you've automated &lt;em&gt;not reviewing&lt;/em&gt;. Layer 2 exists so a human still owns the decision.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Giant PRs sneaking back in.&lt;/strong&gt; AI makes it easy to generate 2,000 lines before lunch. Enforce a size limit, or review capacity collapses again and you're right back at the desk at 11 p.m.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amplifier effect.&lt;/strong&gt; A weak test suite plus a fast code generator plus an AI reviewer is just a faster way to ship bugs with a confident summary attached.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;steal-this-setup&quot;&gt;Steal This Setup&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Formatter, linter, type-checker, tests, secret scan, dependency audit as &lt;strong&gt;required checks&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;[ ] CI actions pinned to &lt;strong&gt;commit SHAs&lt;/strong&gt;, with minimal permissions&lt;/li&gt;
&lt;li&gt;[ ] AI reviewer configured with &lt;strong&gt;written rules&lt;/strong&gt; for your codebase&lt;/li&gt;
&lt;li&gt;[ ] The &lt;strong&gt;Leg Day Test&lt;/strong&gt; run on your own repo before you trust the tool&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;PR size limit&lt;/strong&gt; (a few hundred lines, then split)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;CODEOWNERS&lt;/code&gt; with a &lt;strong&gt;second human&lt;/strong&gt; on auth, crypto, payments, infra and CI config&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Auto-merge&lt;/strong&gt; only for low-risk changes with green checks&lt;/li&gt;
&lt;li&gt;[ ] A calendar block for the gym that gets the same respect as a production incident&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-point-1&quot;&gt;The Point&lt;/h3&gt;
&lt;p&gt;Code should work for you, not the other way around. Automate the boring review work, spend your attention where judgment actually matters, and use the time you win back for the things that keep you functional: sleep, sunlight, and heavy objects that don't compile.&lt;/p&gt;
&lt;p&gt;Your pull request queue will always be full. Your quads, on the other hand, are on a deadline.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; What's the most mind-numbing chore in your workflow that a pipeline could be doing instead? Drop it in the comments and let's crowdsource the automation.&lt;/p&gt;
&lt;p&gt;And if you're reading this hunched over a diff right now: close the tab, stand up, do ten squats. The PR will still be there. Honestly, so will the bug on line 812. But your legs will be thrilled.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30447/i-taught-ai-to-review-my-code-so-i-never-miss-leg-day-again</guid>
<pubDate>Fri, 09 Oct 2026 11:11:20 +0000</pubDate>
</item>
<item>
<title>the evolution of software development since the emergence of AI</title>
<link>https://coderlegion.com/30424/the-evolution-of-software-development-since-the-emergence-of-ai</link>
<description>&lt;p&gt;It would be interesting to promote discussions on the evolution of software development since the emergence of AI, and how it has been transforming the sector and the jobs of many developers.   In my experience, it's not advisable to swim against the current, so it would also be valuable to create a dedicated section to discuss and build technical AI applications that can boost our productivity and allow us to create more complex, useful developments that non-technical users cannot achieve.&lt;/p&gt;
&lt;p&gt;Do you think the emergence of AI is a positive or negative event for software developers?&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30424/the-evolution-of-software-development-since-the-emergence-of-ai</guid>
<pubDate>Fri, 09 Oct 2026 09:51:04 +0000</pubDate>
</item>
<item>
<title>Mi Definition of Done: 5 gates que convierten &quot;creo que funciona&quot; en &quot;puedo demostrarlo&quot;</title>
<link>https://coderlegion.com/30228/mi-definition-of-done-5-gates-que-convierten-creo-que-funciona-en-puedo-demostrarlo</link>
<description>&amp;lt;!--
  CoderLegion · Article (Discussion-style) · Post 03
  Serie: — (standalone)
  Autor: Ignacio Badenes (@yosoyignicion / IgnicionDev)
  Idioma: bilingüe ES + EN · Read time: ~8 min
  Tags: python, security, testing, opensource
  Caso real: pass-encrypt-env (uso personal, Linux Mint) — https://github.com/yosoyignicion/pass-encrypt-env
  Nota: mini-app en estado &quot;uso personal&quot;; la adaptación/portabilidad se planifica en días próximos.
--&amp;gt;
&lt;h2 id=&quot;mi-definition-of-done-5-gates-que-convierten-creo-que-funciona-en-puedo-demostrarlo-&quot;&gt;Mi Definition of Done: 5 gates que convierten &quot;creo que funciona&quot; en &quot;puedo demostrarlo&quot;&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Nota honesta:&lt;/strong&gt; esto no es un producto, es una &lt;strong&gt;mini-app de uso personal&lt;/strong&gt; en mi Linux Mint. Funciona y la uso a diario; su adaptación para terceros la planifico para los próximos días.&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Etiquetas:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;testing&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;un-commit-una-hora-y-no-es-un-juguete&quot;&gt;Un commit. Una hora. Y no es un juguete&lt;/h3&gt;
&lt;p&gt;El proyecto del que hablo hoy tiene &lt;strong&gt;un solo commit&lt;/strong&gt; y tardó &lt;strong&gt;una hora&lt;/strong&gt; en funcionar de punta a punta. Lo uso cada mañana. Y no es un juguete.&lt;/p&gt;
&lt;p&gt;Lo digo porque la pregunta interesante no fue &lt;em&gt;cómo&lt;/em&gt; lo programé rápido, sino &lt;strong&gt;cómo supe que estaba terminado&lt;/strong&gt;. Ese es el punto: &quot;terminado&quot; no es una sensación, es una decisión que puedes defender. Cuando esa decisión tiene una definición clara, programar una hora deja de ser un atajo y pasa a ser consecuencia.&lt;/p&gt;
&lt;p&gt;Este post es esa definición. La llamo &lt;em&gt;Definition of Done&lt;/em&gt; y son cinco gates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;qu-hace-sin-tecnicismos&quot;&gt;Qué hace, sin tecnicismos&lt;/h3&gt;
&lt;p&gt;Imagina un llavero. Guarda tus claves API y credenciales &lt;strong&gt;cifradas dentro de tu propio ordenador&lt;/strong&gt; y se las sirve a tus aplicaciones durante toda la sesión después de &lt;strong&gt;un único desbloqueo&lt;/strong&gt; al iniciar sesión. Nada sube a la nube. No hay telemetría. No hay cuenta que crear.&lt;/p&gt;
&lt;p&gt;En la práctica, mis scripts y programas ya no llevan claves escritas dentro ni dependen de un &lt;code&gt;.env&lt;/code&gt; en claro: consultan el llavero, que ya está abierto por mí, y arrancan &lt;strong&gt;sin volver a pedirme nada&lt;/strong&gt;. Si me voy, el llavero se cierra; las claves desaparecen de la memoria.&lt;/p&gt;
&lt;p&gt;Se llama &lt;code&gt;pass-encrypt-env&lt;/code&gt;. Corre en mi Linux Mint y, hoy, es para mí.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lo-que-es-hoy-y-lo-que-todav-a-no-&quot;&gt;Lo que es hoy (y lo que todavía no)&lt;/h3&gt;
&lt;p&gt;Ser claro con el estado real es parte del método. Un proyecto &quot;terminado&quot; también &lt;strong&gt;declara sus límites&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Es:&lt;/strong&gt; una herramienta personal, funcional, que uso a diario en mi equipo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No es:&lt;/strong&gt; un producto instalable por cualquiera sin fricción, ni algo auditado por terceros.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Viene:&lt;/strong&gt; en los próximos días planifico la &lt;strong&gt;adaptación&lt;/strong&gt; —empaquetado, arranque en otras distribuciones y documentación para quien no sea yo— como un esfuerzo aparte y planificado.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Y hay una razón de fondo para separar esa adaptación del desarrollo: no quiero declarar &quot;lista&quot; una app porque pase &lt;strong&gt;en mi máquina&lt;/strong&gt;. La declaro lista cuando pasa los gates &lt;strong&gt;en cualquier máquina&lt;/strong&gt;. Es exactamente la misma lógica que un buen test: no vale que funcione una vez, tiene que fallar cuando corresponda.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;por-qu-exijo-evidencia-y-no-sensaciones&quot;&gt;Por qué exijo evidencia y no sensaciones&lt;/h3&gt;
&lt;p&gt;Soy relativamente nuevo en el mundo de la IA agéntica, aunque llevo años gestionando sistemas bajo protocolo: control de accesos, mínima exposición, registros. Y una disciplina que aprendí ahí —y que el prompting me recordó— es esta:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;No das algo por resuelto porque &lt;em&gt;parezca&lt;/em&gt; resuelto; lo das por resuelto cuando puedes &lt;strong&gt;demostrarlo&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;El hábito de protocolo es la &lt;em&gt;mínima exposición&lt;/em&gt;: asumes que algo fallará y diseñas para que, cuando falle, no arrastre contigo todo lo demás. Aplicado al software, eso se traduce en una &lt;em&gt;Definition of Done&lt;/em&gt;: una lista corta de condiciones que convierten &lt;em&gt;&quot;yo creo que funciona&quot;&lt;/em&gt; en &lt;em&gt;&quot;aquí está la prueba&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;En este proyecto, esa lista lleva cinco puertas.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;los-5-gates&quot;&gt;Los 5 gates&lt;/h3&gt;
&lt;p&gt;Cada gate existe porque previene un fallo concreto, no por completitud.&lt;/p&gt;
&lt;h4 id=&quot;1-tests-que-pueden-fallar&quot;&gt;1) Tests que pueden fallar&lt;/h4&gt;
&lt;p&gt;No me sirve &quot;tengo tests&quot;; me sirve que fallen cuando rompo algo. En &lt;code&gt;pass-encrypt-env&lt;/code&gt; hay &lt;strong&gt;19 tests&lt;/strong&gt; y uno de los que más valor tiene es este, porque vigila una promesa sobre el disco:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_no_plaintext_on_disk(tmp_path):
    path = tmp_path / &quot;vault.enc&quot;
    v, unlocked, _ = _new()
    vault.set_secret(unlocked.payload, &quot;TOKEN&quot;, &quot;super-secret-value&quot;)
    vault.reseal(v, unlocked.dek, unlocked.payload)
    vault.save_vault(v, path)

    blob = path.read_text()
    assert &quot;super-secret-value&quot; not in blob          # el secreto no está en claro
    assert (path.stat().st_mode &amp;amp; 0o777) == 0o600     # y el fichero no es legible por terceros
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ese test no comprueba que &quot;va bien&quot;: comprueba que &lt;strong&gt;la promesa del producto se cumple&lt;/strong&gt;. El fallo que previene es el peor de todos: creerte seguro y no serlo.&lt;/p&gt;
&lt;h4 id=&quot;2-lint-limpio&quot;&gt;2) Lint limpio&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ruff check .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No es estética. Un lint en verde elimina de antemano la clase de error más tonta —imports sin usar, variables fantasma, comparaciones sospechosas— antes de que la ejecutes. Es ruido que no llega a producción porque ni siquiera llega al commit.&lt;/p&gt;
&lt;h4 id=&quot;3-escaneo-de-seguridad&quot;&gt;3) Escaneo de seguridad&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bandit -q -c bandit.yaml -r src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bandit&lt;/code&gt; busca patrones inseguros en el código: criptografía mal usada, &lt;code&gt;subprocess&lt;/code&gt; peligrosos, aleatoriedad no criptográfica. En una app cuyo único trabajo es guardar secretos, este gate no es opcional. El fallo que previene es el uso accidental de una primitiva &lt;em&gt;parecida&lt;/em&gt; a la correcta.&lt;/p&gt;
&lt;h4 id=&quot;4-ci-reproducible&quot;&gt;4) CI reproducible&lt;/h4&gt;
&lt;p&gt;Los tres gates anteriores corren solos en GitHub Actions, sobre &lt;strong&gt;Python 3.11 y 3.12&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;strategy:
  fail-fast: false
  matrix:
    python-version: [&quot;3.11&quot;, &quot;3.12&quot;]
steps:
  # ...
  - name: Gate (ruff + bandit + pytest)
    run: ./scripts/ci.sh python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y aquí está la parte importante: ese &lt;code&gt;./scripts/ci.sh&lt;/code&gt; &lt;strong&gt;es el mismo comando que ejecuto en local&lt;/strong&gt;. No hay una verdad para mi máquina y otra para el servidor. El fallo que previene este gate es &quot;en mi máquina funciona&quot;, que no es un bug: es una mentira cómoda.&lt;/p&gt;
&lt;h4 id=&quot;5-smoke-end-to-end&quot;&gt;5) Smoke end-to-end&lt;/h4&gt;
&lt;p&gt;Los unit tests verdes no garantizan que el sistema entero arranque. Para eso hay un &lt;em&gt;smoke test&lt;/em&gt; headless que levanta el agente, habla con él por su canal interno y comprueba el comportamiento observable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m pass_encrypt_env.cli env --if-unlocked &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || rc=$?
echo &quot;env --if-unlocked (bloqueada) -&amp;gt; exit $rc (esperado 3)&quot;
[ &quot;$rc&quot; -eq 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aun con piezas perfectas, el conjunto puede no montarse. El smoke test es el gate que mira el &lt;strong&gt;sistema&lt;/strong&gt;, no las partes.&lt;/p&gt;
&lt;h4 id=&quot;la-meta-regla&quot;&gt;La meta-regla&lt;/h4&gt;
&lt;p&gt;Todos los gates caben en un comando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./scripts/ci.sh    # esto corre en local y esto corre en CI. La misma verdad.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;qu-protege-y-qu-no&quot;&gt;Qué protege y qué no&lt;/h3&gt;
&lt;p&gt;Un proyecto terminado con honestidad &lt;strong&gt;publica su techo&lt;/strong&gt;, no lo esconde. Este es el mío:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Amenaza &lt;/th&gt;&lt;th align=&quot;center&quot;&gt;¿Mitigado?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Robo offline de &lt;code&gt;vault.enc&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Sí — sin master + TOTP no hay datos&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Manipulación del fichero &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Sí — AEAD detecta la alteración&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Alguien sin tu TOTP &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Sí — segundo factor obligatorio&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Pérdida de master o TOTP &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Sí — clave de recuperación&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Proceso del &lt;strong&gt;mismo UID&lt;/strong&gt; con la bóveda abierta &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;No — techo del diseño cero-prompt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;root&lt;/code&gt; / malware ya dentro &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;No — fuera de alcance&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Que la última fila diga &quot;No&quot; no es una debilidad: es una promesa cumplible. Prefiero un alcance pequeño y cierto que uno grande y falso.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;detalle-t-cnico-para-quien-quiera-el-fondo-&quot;&gt;Detalle técnico (para quien quiera el fondo)&lt;/h3&gt;
&lt;p&gt;El &quot;cómo&quot;, para quien lea esto con la ingeniería delante:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cifrado en reposo:&lt;/strong&gt; &lt;code&gt;vault.enc&lt;/code&gt; con &lt;strong&gt;Argon2id&lt;/strong&gt; como función de derivación (64 MiB, 3 pasadas, 4 lanes) y &lt;strong&gt;AES-256-GCM&lt;/strong&gt; como cifrado autenticado (confidencialidad e integridad).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doble envoltura de la clave de datos (DEK):&lt;/strong&gt; &lt;code&gt;K1 = Argon2id(master)&lt;/code&gt; · &lt;code&gt;K2 = HKDF(K1 ‖ totp_secret)&lt;/code&gt; abre la DEK · &lt;code&gt;K3 = HKDF(recovery_key)&lt;/code&gt; guarda una copia para recuperación. El secreto TOTP viaja cifrado: sin un TOTP válido no se obtiene la DEK.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agente de sesión:&lt;/strong&gt; escucha en un &lt;em&gt;socket&lt;/em&gt; Unix en &lt;code&gt;$XDG_RUNTIME_DIR/passenv/&lt;/code&gt;, permisos &lt;code&gt;0600&lt;/code&gt; y validación de UID del par (&lt;code&gt;SO_PEERCRED&lt;/code&gt;). Se endurece con &lt;code&gt;PR_SET_DUMPABLE=0&lt;/code&gt; (anti-&lt;em&gt;ptrace&lt;/em&gt;), sin volcados de memoria y &lt;code&gt;mlockall&lt;/code&gt; &lt;em&gt;best-effort&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI &lt;code&gt;passenv&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;set/get/list/rm&lt;/code&gt;, &lt;code&gt;export&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, &lt;code&gt;change-master&lt;/code&gt;, &lt;code&gt;backup&lt;/code&gt;, &lt;code&gt;restore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modelo de amenazas completo&lt;/strong&gt; en &lt;code&gt;docs/THREAT_MODEL.md&lt;/code&gt;, con el techo del diseño cero-prompt declarado a propósito.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lo-que-esto-cambia-takeaway-&quot;&gt;Lo que esto cambia (Takeaway)&lt;/h3&gt;
&lt;p&gt;El gate no es un freno; es lo que te deja pisar el acelerador sin miedo. Un proyecto de una hora y un commit no llega &quot;terminado de verdad&quot; por suerte: llega porque las condiciones para llamarlo terminado &lt;strong&gt;ya estaban escritas&lt;/strong&gt; antes de empezar a escribir código.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Creo que funciona&quot; es una sensación. &quot;Aquí está la prueba&quot; es un proyecto.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;la-checklist&quot;&gt;La checklist&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] Tests que fallan cuando rompo algo (no que existan)
- [ ] Lint limpio
- [ ] Escaneo de seguridad sin hallazgos
- [ ] CI reproducible (mismo comando en local que en remoto)
- [ ] Smoke end-to-end en verde
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;y-t-&quot;&gt;Y tú&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;¿Qué requisitos le exiges tú a un proyecto para darlo por terminado? ¿Cuál añadirías a mis cinco gates?&lt;/strong&gt; Y, ya que el mío está en estado de uso personal: &lt;strong&gt;¿tú publicarías una herramienta así o la dejarías privada hasta &quot;adaptarla&quot;?&lt;/strong&gt; Cuéntamelo en los comentarios; leo y respondo a todo.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;&lt;strong&gt;English version below&lt;/strong&gt; · El original está en español. If you're an English speaker, scroll down to the translation and tell me what your own Definition of Done looks like. I reply to everyone.&lt;/sub&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-definition-of-done-5-gates-that-turn-i-think-it-works-into-here-s-the-proof-&quot;&gt;My Definition of Done: 5 gates that turn &quot;I think it works&quot; into &quot;here's the proof&quot;&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Honest note:&lt;/strong&gt; this isn't a product, it's a &lt;strong&gt;personal-use mini-app&lt;/strong&gt; on my Linux Mint. It works and I use it daily; adapting it for other people is a planned effort for the coming days.&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;testing&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;one-commit-one-hour-and-it-s-not-a-toy&quot;&gt;One commit. One hour. And it's not a toy&lt;/h3&gt;
&lt;p&gt;The project I'm talking about has &lt;strong&gt;a single commit&lt;/strong&gt; and took &lt;strong&gt;one hour&lt;/strong&gt; to work end to end. I use it every morning. And it isn't a toy.&lt;/p&gt;
&lt;p&gt;The interesting question wasn't &lt;em&gt;how&lt;/em&gt; I built it fast, but &lt;strong&gt;how I knew it was done&lt;/strong&gt;. That's the point: &quot;done&quot; isn't a feeling, it's a decision you can defend. Once that decision has a clear definition, writing code for an hour stops being a shortcut and becomes a consequence.&lt;/p&gt;
&lt;p&gt;This post is that definition. I call it a &lt;em&gt;Definition of Done&lt;/em&gt;, and it's five gates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-does-without-jargon&quot;&gt;What it does, without jargon&lt;/h3&gt;
&lt;p&gt;Picture a keyring. It keeps your API keys and credentials &lt;strong&gt;encrypted inside your own computer&lt;/strong&gt; and serves them to your applications for the whole session after &lt;strong&gt;a single unlock&lt;/strong&gt; at login. Nothing goes to the cloud. There's no telemetry. There's no account to create.&lt;/p&gt;
&lt;p&gt;In practice, my scripts and apps no longer carry keys inside them or depend on a plaintext &lt;code&gt;.env&lt;/code&gt;: they ask the keyring, already unlocked on my behalf, and start &lt;strong&gt;without asking me again&lt;/strong&gt;. If I leave, the keyring closes; the keys disappear from memory.&lt;/p&gt;
&lt;p&gt;It's called &lt;code&gt;pass-encrypt-env&lt;/code&gt;. It runs on my Linux Mint and, today, it's for me.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-is-today-and-what-it-isn-t-yet-&quot;&gt;What it is today (and what it isn't yet)&lt;/h3&gt;
&lt;p&gt;Being clear about the real state is part of the method. A &quot;finished&quot; project also &lt;strong&gt;declares its limits&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It is:&lt;/strong&gt; a personal, working tool that I use daily on my machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It isn't:&lt;/strong&gt; something anyone can install without friction, nor something audited by third parties.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It's coming:&lt;/strong&gt; in the coming days I'm planning the &lt;strong&gt;adaptation&lt;/strong&gt; — packaging, booting on other distributions and documentation for someone who isn't me — as a separate, planned effort.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There's a deeper reason to separate that adaptation from development: I don't want to call an app &quot;ready&quot; because it passes &lt;strong&gt;on my machine&lt;/strong&gt;. I call it ready when it passes the gates &lt;strong&gt;on any machine&lt;/strong&gt;. It's the same logic as a good test: it doesn't count if it works once, it has to fail when it should.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-demand-evidence-not-feelings&quot;&gt;Why I demand evidence, not feelings&lt;/h3&gt;
&lt;p&gt;I'm relatively new to the world of agentic AI, though I've spent years managing systems under protocol: access control, minimal exposure, logs. And one discipline I learned there — which prompting reminded me of — is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;You don't call something solved because it &lt;em&gt;looks&lt;/em&gt; solved; you call it solved when you can &lt;strong&gt;prove it&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The protocol habit is &lt;em&gt;minimal exposure&lt;/em&gt;: you assume something will fail and you design so that, when it fails, it doesn't drag everything else down with it. Applied to software, that becomes a &lt;em&gt;Definition of Done&lt;/em&gt;: a short list of conditions that turn &lt;em&gt;&quot;I think it works&quot;&lt;/em&gt; into &lt;em&gt;&quot;here's the proof&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In this project, that list has five doors.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-5-gates&quot;&gt;The 5 gates&lt;/h3&gt;
&lt;p&gt;Each gate exists because it prevents a concrete failure, not for completeness.&lt;/p&gt;
&lt;h4 id=&quot;1-tests-that-can-fail&quot;&gt;1) Tests that can fail&lt;/h4&gt;
&lt;p&gt;I don't care about &quot;I have tests&quot;; I care that they fail when I break something. &lt;code&gt;pass-encrypt-env&lt;/code&gt; has &lt;strong&gt;19 tests&lt;/strong&gt;, and one of the most valuable guards a promise about the disk:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_no_plaintext_on_disk(tmp_path):
    path = tmp_path / &quot;vault.enc&quot;
    v, unlocked, _ = _new()
    vault.set_secret(unlocked.payload, &quot;TOKEN&quot;, &quot;super-secret-value&quot;)
    vault.reseal(v, unlocked.dek, unlocked.payload)
    vault.save_vault(v, path)

    blob = path.read_text()
    assert &quot;super-secret-value&quot; not in blob          # the secret isn't in cleartext
    assert (path.stat().st_mode &amp;amp; 0o777) == 0o600     # and the file isn't readable by others
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That test doesn't check that &quot;it works&quot;: it checks that &lt;strong&gt;the product's promise holds&lt;/strong&gt;. The failure it prevents is the worst of all: believing you're safe when you aren't.&lt;/p&gt;
&lt;h4 id=&quot;2-clean-lint&quot;&gt;2) Clean lint&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ruff check .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It isn't aesthetics. A green lint removes the dumbest class of error —unused imports, ghost variables, suspicious comparisons— before you even run it. It's noise that never reaches production because it never reaches the commit.&lt;/p&gt;
&lt;h4 id=&quot;3-security-scan&quot;&gt;3) Security scan&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bandit -q -c bandit.yaml -r src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bandit&lt;/code&gt; looks for insecure patterns in the code: misused cryptography, dangerous &lt;code&gt;subprocess&lt;/code&gt;, non-cryptographic randomness. In an app whose only job is to store secrets, this gate isn't optional. The failure it prevents is the accidental use of a primitive that &lt;em&gt;looks&lt;/em&gt; like the right one.&lt;/p&gt;
&lt;h4 id=&quot;4-reproducible-ci&quot;&gt;4) Reproducible CI&lt;/h4&gt;
&lt;p&gt;The previous three gates run on their own in GitHub Actions, across &lt;strong&gt;Python 3.11 and 3.12&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;strategy:
  fail-fast: false
  matrix:
    python-version: [&quot;3.11&quot;, &quot;3.12&quot;]
steps:
  # ...
  - name: Gate (ruff + bandit + pytest)
    run: ./scripts/ci.sh python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here's the important part: that &lt;code&gt;./scripts/ci.sh&lt;/code&gt; &lt;strong&gt;is the same command I run locally&lt;/strong&gt;. There's no one truth for my machine and another for the server. The failure this gate prevents is &quot;it works on my machine&quot;, which isn't a bug: it's a comfortable lie.&lt;/p&gt;
&lt;h4 id=&quot;5-end-to-end-smoke-test&quot;&gt;5) End-to-end smoke test&lt;/h4&gt;
&lt;p&gt;Green unit tests don't guarantee the whole system boots. For that there's a headless &lt;em&gt;smoke test&lt;/em&gt; that starts the agent, talks to it over its internal channel and checks the observable behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m pass_encrypt_env.cli env --if-unlocked &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || rc=$?
echo &quot;env --if-unlocked (locked) -&amp;gt; exit $rc (expected 3)&quot;
[ &quot;$rc&quot; -eq 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even with perfect pieces, the whole may fail to assemble. The smoke test is the gate that looks at the &lt;strong&gt;system&lt;/strong&gt;, not the parts.&lt;/p&gt;
&lt;h4 id=&quot;the-meta-rule&quot;&gt;The meta-rule&lt;/h4&gt;
&lt;p&gt;Every gate fits in one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./scripts/ci.sh    # this runs locally and this runs in CI. The same truth.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-protects-and-what-it-doesn-t&quot;&gt;What it protects and what it doesn't&lt;/h3&gt;
&lt;p&gt;An honestly finished project &lt;strong&gt;publishes its ceiling&lt;/strong&gt;, it doesn't hide it. This is mine:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Threat &lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Mitigated?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Offline theft of &lt;code&gt;vault.enc&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Yes — without master + TOTP there's no data&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;File tampering &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Yes — AEAD detects the alteration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Someone without your TOTP &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Yes — second factor required&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Loss of master or TOTP &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;Yes — recovery key&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;A process of the &lt;strong&gt;same UID&lt;/strong&gt; with the vault open &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;No — ceiling of the prompt-less design&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;root&lt;/code&gt; / malware already inside &lt;/td&gt;&lt;td align=&quot;center&quot;&gt;No — out of scope&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That the last row says &quot;No&quot; isn't a weakness: it's a promise you can keep. I'd rather have a small and certain scope than a large and false one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technical-detail-for-those-who-want-the-depth-&quot;&gt;Technical detail (for those who want the depth)&lt;/h3&gt;
&lt;p&gt;The &quot;how&quot;, for whoever reads this with the engineering in front of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Encryption at rest:&lt;/strong&gt; &lt;code&gt;vault.enc&lt;/code&gt; with &lt;strong&gt;Argon2id&lt;/strong&gt; as the key-derivation function (64 MiB, 3 passes, 4 lanes) and &lt;strong&gt;AES-256-GCM&lt;/strong&gt; as authenticated encryption (confidentiality and integrity).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-layer wrapping of the data key (DEK):&lt;/strong&gt; &lt;code&gt;K1 = Argon2id(master)&lt;/code&gt; · &lt;code&gt;K2 = HKDF(K1 ‖ totp_secret)&lt;/code&gt; unwraps the DEK · &lt;code&gt;K3 = HKDF(recovery_key)&lt;/code&gt; keeps a recovery copy. The TOTP secret travels encrypted: without a valid TOTP you don't get the DEK.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session agent:&lt;/strong&gt; it listens on a Unix &lt;em&gt;socket&lt;/em&gt; at &lt;code&gt;$XDG_RUNTIME_DIR/passenv/&lt;/code&gt;, mode &lt;code&gt;0600&lt;/code&gt; and UID validation of the peer (&lt;code&gt;SO_PEERCRED&lt;/code&gt;). It's hardened with &lt;code&gt;PR_SET_DUMPABLE=0&lt;/code&gt; (anti-&lt;em&gt;ptrace&lt;/em&gt;), no memory dumps and best-effort &lt;code&gt;mlockall&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;passenv&lt;/code&gt; CLI:&lt;/strong&gt; &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;set/get/list/rm&lt;/code&gt;, &lt;code&gt;export&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, &lt;code&gt;change-master&lt;/code&gt;, &lt;code&gt;backup&lt;/code&gt;, &lt;code&gt;restore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full threat model&lt;/strong&gt; in &lt;code&gt;docs/THREAT_MODEL.md&lt;/code&gt;, with the prompt-less design's ceiling declared on purpose.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-changes-takeaway-&quot;&gt;What this changes (Takeaway)&lt;/h3&gt;
&lt;p&gt;The gate isn't a brake; it's what lets you hit the accelerator without fear. A one-hour, one-commit project doesn't end up &quot;truly finished&quot; by luck: it does because the conditions to call it finished were &lt;strong&gt;written before the code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;I think it works&quot; is a feeling. &quot;Here's the proof&quot; is a project.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-checklist-1&quot;&gt;The checklist&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- [ ] Tests that fail when I break something (not that they exist)
- [ ] Clean lint
- [ ] Security scan with no findings
- [ ] Reproducible CI (same command locally and remotely)
- [ ] End-to-end smoke test green
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;over-to-you&quot;&gt;Over to you&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;What requirements do you demand from a project before calling it done? Which one would you add to my five gates?&lt;/strong&gt; And since mine is in a personal-use state: &lt;strong&gt;would you publish a tool like this or keep it private until you &quot;adapt&quot; it?&lt;/strong&gt; Tell me in the comments; I read and reply to everyone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;Project license: MIT · No secrets or keys in this post. · Project: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion/pass-encrypt-env&quot;&gt;github.com/yosoyignicion/pass-encrypt-env&lt;/a&gt; · Profile: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion&quot;&gt;github.com/yosoyignicion&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30228/mi-definition-of-done-5-gates-que-convierten-creo-que-funciona-en-puedo-demostrarlo</guid>
<pubDate>Thu, 08 Oct 2026 07:28:13 +0000</pubDate>
</item>
<item>
<title>Tu backup no sirve si no puedes restaurarlo: el -wal que vació mi base</title>
<link>https://coderlegion.com/30129/tu-backup-no-sirve-si-no-puedes-restaurarlo-el-wal-que-vacio-mi-base</link>
<description>&lt;h2 id=&quot;tu-backup-no-sirve-si-no-puedes-restaurarlo-el--wal-que-vaci-mi-base&quot;&gt;Tu backup no sirve si no puedes restaurarlo: el &lt;code&gt;-wal&lt;/code&gt; que vació mi base&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Serie:&lt;/strong&gt; &lt;em&gt;Software auditable&lt;/em&gt; — Post 02&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~10-12 · &lt;strong&gt;Etiquetas:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;database&lt;/code&gt; &lt;code&gt;testing&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;la-confesi-n-un-bot-n-que-dec-a-respaldo-y-no-garantizaba-nada&quot;&gt;La confesión: un botón que decía &quot;RESPALDO&quot; y no garantizaba nada&lt;/h3&gt;
&lt;p&gt;Tengo una regla que me ha costado aprender: &lt;strong&gt;desconfía de tus propias features hasta que puedas demostrarlas&lt;/strong&gt;. Durante un tiempo, Aethernet tuvo un botón grande, verde y tranquilizador que decía &lt;code&gt;RESPALDO .TAR.GZ&lt;/code&gt;. Lo pulsabas, salía un archivo, y te ibas a dormir tranquilo. No garantizaba absolutamente nada.&lt;/p&gt;
&lt;p&gt;Ese botón es el punto de partida de este post. No porque estuviera roto —creaba un archivo, sí—, sino porque &lt;strong&gt;parecía una promesa y era una hipótesis sin comprobar&lt;/strong&gt;: nunca nadie había restaurado esa copia en otro equipo. Yo no había hecho la única pregunta que importa: ¿funciona cuando la necesitas?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-feedback-que-lo-puso-por-escrito&quot;&gt;El feedback que lo puso por escrito&lt;/h3&gt;
&lt;p&gt;Un amigo de la comunidad me devolvió, con ayuda de su modelo agéntico como copiloto, un análisis que resumía el problema mejor que yo. Su idea central cabe en una frase: &lt;strong&gt;exportar no es recuperar&lt;/strong&gt;. Y me propuso un criterio de aceptación concreto: una prueba de recuperación &lt;em&gt;sin conexión&lt;/em&gt;, en una máquina limpia, sin acceso a la instalación original, en la que el usuario pueda examinar el historial y generar un informe. &quot;La capacidad de exportación y la capacidad de recuperación son criterios de aceptación distintos.&quot;&lt;/p&gt;
&lt;p&gt;Tenía razón, y además señalaba el hueco exacto de mi proyecto: no existía un procedimiento de recuperación documentado que cubriera base de datos, configuración y versión de esquema, ni claridad sobre qué claves había que guardar aparte.&lt;/p&gt;
&lt;p&gt;Ese comentario convirtió una intuición en una &lt;strong&gt;lista de tareas verificables&lt;/strong&gt;. Lo demás de este post es cómo lo cerré.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-problema-en-el-c-digo&quot;&gt;El problema, en el código&lt;/h3&gt;
&lt;p&gt;Mi respaldo era literalmente esto:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# El botón honesto y equivocado
target = paths.data_dir / f&quot;aethernet-backup-{ts}.tar.gz&quot;
with tarfile.open(target, &quot;w:gz&quot;) as tar:
    tar.add(paths.db_path, arcname=&quot;aethernet.db&quot;)   # solo la base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tiene cuatro problemas, y los cuatro importan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Solo copia la base.&lt;/strong&gt; La configuración (&lt;code&gt;my_ssids&lt;/code&gt;, alias, retención…) se queda fuera.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No registra la versión del esquema.&lt;/strong&gt; Al restaurar, no sabrías si la copia es más vieja o más nueva que la app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copia un fichero vivo.&lt;/strong&gt; La base corre en modo &lt;strong&gt;WAL&lt;/strong&gt;; copiar el &lt;code&gt;.db&lt;/code&gt; a pelo mientras hay escrituras puede darte un estado incompleto.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No hay restore.&lt;/strong&gt; Un backup sin restauración es un export con buenos modales.&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Exportar &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Recuperar&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Pregunta &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;¿se leen bien los datos? &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;¿arranco como si nada hubiera pasado?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Máquina &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;esta, con la app instalada &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;limpia, sin nada de la original&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Prueba &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;abrir el informe &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;restaurar y volver a medir&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;la-soluci-n-en-cuatro-piezas&quot;&gt;La solución, en cuatro piezas&lt;/h3&gt;
&lt;h4 id=&quot;1-un-paquete-que-se-puede-inspeccionar&quot;&gt;1) Un paquete que se puede inspeccionar&lt;/h4&gt;
&lt;p&gt;Un único &lt;code&gt;.tar.gz&lt;/code&gt; con tres piezas y un manifiesto legible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;app&quot;: &quot;aethernet&quot;,
  &quot;format_version&quot;: 1,
  &quot;app_version&quot;: &quot;1.3.0&quot;,
  &quot;schema_version&quot;: 5,
  &quot;created_at&quot;: 1759800000.0,
  &quot;db_sha256&quot;: &quot;9f2c…&quot;,
  &quot;members&quot;: [&quot;aethernet.db&quot;, &quot;config.toml&quot;, &quot;manifest.json&quot;],
  &quot;secrets_required&quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ese &lt;code&gt;secrets_required: []&lt;/code&gt; no es decoración: es una declaración de diseño. &lt;strong&gt;No hay claves irrecuperables que guardar aparte.&lt;/strong&gt; El token de la API se regenera y nunca viaja en el paquete.&lt;/p&gt;
&lt;h4 id=&quot;2-copia-consistente-y-por-qu-no-un-cp-&quot;&gt;2) Copia consistente (y por qué no un &lt;code&gt;cp&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Con WAL activo, copiar el fichero de la base es una apuesta. La forma correcta es dejar que SQLite haga la copia:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;src = sqlite3.connect(str(db_path))
dst = sqlite3.connect(str(staged_db))
src.backup(dst)          # instantánea consistente, aunque haya un escritor vivo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Connection.backup()&lt;/code&gt; fusiona el WAL en un único fichero coherente. Es stdlib: cero dependencias nuevas.&lt;/p&gt;
&lt;h4 id=&quot;3-restaurar-sin-romper-nada&quot;&gt;3) Restaurar sin romper nada&lt;/h4&gt;
&lt;p&gt;El orden importa, y cada paso existe por un fallo real:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validar antes de escribir&lt;/strong&gt; — SHA-256 del manifiesto, miembros obligatorios y rutas seguras (nada de &lt;code&gt;../&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rechazar el futuro&lt;/strong&gt; — si la copia usa un esquema más nuevo que el binario, se aborta con un mensaje claro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resguardar lo actual&lt;/strong&gt; — la base presente se copia a &lt;code&gt;aethernet.db.bak-&amp;lt;fecha&amp;gt;&lt;/code&gt; antes de tocarla.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrar hacia adelante&lt;/strong&gt; — las migraciones son &lt;em&gt;append-only&lt;/em&gt;, así que una copia más antigua se actualiza sola al restaurar.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;4-el-criterio-convertido-en-test&quot;&gt;4) El criterio convertido en test&lt;/h4&gt;
&lt;p&gt;Aquí está el corazón del asunto. El criterio de aceptación del comentario dejó de ser una opinión y pasó a ser código que puede fallar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_offline_recovery_on_clean_machine(tmp_path):
    # 1. origen: historial + ajustes
    source = _paths(tmp_path, &quot;origin&quot;)
    ...  # se guardan escaneos y config
    bundle = create_backup(source, tmp_path / &quot;backup.tar.gz&quot;)

    # 2. máquina limpia: otra instalación, sin nada de la original
    clean = _paths(tmp_path, &quot;clean&quot;)
    restore_backup(bundle.path, clean)

    # 3. ¿se lee el historial y se genera un informe desde cero?
    repo = Repository(Database(clean.db_path))
    assert repo.wifi_scan_count() == 2
    report = build_report_data(repo, load_settings(clean))
    export(report, &quot;md&quot;, clean.report_dir / &quot;recovery.md&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si este test se pone en rojo, mi backup &lt;strong&gt;no existe&lt;/strong&gt;. No hay interpretación: o recupera, o no recupera.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-bug-el--wal-que-vaci-mi-base-restaurada&quot;&gt;El bug: el &lt;code&gt;-wal&lt;/code&gt; que vació mi base restaurada&lt;/h3&gt;
&lt;p&gt;La primera versión del restore pasaba los tests unitarios… y fallaba el de recuperación real. Restauraba una copia con datos y la base aparecía &lt;strong&gt;vacía&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;La causa raíz es una lección de SQLite que me llevé grabada: al reemplazar &lt;code&gt;aethernet.db&lt;/code&gt; con un &lt;code&gt;os.replace&lt;/code&gt;, los ficheros auxiliares &lt;code&gt;aethernet.db-wal&lt;/code&gt; y &lt;code&gt;aethernet.db-shm&lt;/code&gt; &lt;strong&gt;se quedan ahí&lt;/strong&gt;. Al reabrir la base restaurada, SQLite encuentra un WAL que pertenece a &lt;em&gt;otra&lt;/em&gt; base, lo &lt;strong&gt;reaplica&lt;/strong&gt; y sobrescribe tus datos con el estado anterior.&lt;/p&gt;
&lt;p&gt;El arreglo va en dos capas, porque el fallo tenía dos puertas:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;_remove_sidecars(paths.db_path)          # fuera el -wal/-shm huérfano
os.replace(staged_db, paths.db_path)
_remove_sidecars(paths.db_path)          # y otra vez, por si acaso
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…y en el CLI, &lt;strong&gt;cerrar la conexión del contexto antes de restaurar&lt;/strong&gt;, porque no se restaura sobre una base abierta. Añadí un test que restaura &lt;em&gt;sobre una conexión WAL viva&lt;/em&gt; para que no vuelva a pasar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;live = Database(paths.db_path); live.migrate()
restore_backup(bundle.path, paths)       # antes: 0 escaneos. ahora: 1.
assert Repository(Database(paths.db_path)).wifi_scan_count() == 1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-otro-bug-que-no-ves-hasta-que-cambias-de-disco&quot;&gt;El otro bug que no ves hasta que cambias de disco&lt;/h3&gt;
&lt;p&gt;Con el test en verde, probé en un disco distinto y apareció &lt;code&gt;OSError: Invalid cross-device link&lt;/code&gt;. El &lt;em&gt;staging&lt;/em&gt; vivía en &lt;code&gt;/tmp&lt;/code&gt; (a menudo &lt;code&gt;tmpfs&lt;/code&gt;) y el &lt;code&gt;os.replace&lt;/code&gt; final apuntaba a &lt;code&gt;~/.local/share&lt;/code&gt;, en otro sistema de ficheros: un renombrado no puede cruzar discos.&lt;/p&gt;
&lt;p&gt;La corrección es de una línea pero de fondo: &lt;strong&gt;el staging vive junto al destino&lt;/strong&gt;, para que el reemplazo sea un renombrado atómico en el mismo sistema de ficheros. Para la configuración, un &lt;code&gt;shutil.move&lt;/code&gt; (que sí sabe cruzar discos). Lo verifiqué con la base en &lt;code&gt;tmpfs&lt;/code&gt; y el temporal en disco: recuperación correcta.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lo-que-esto-cambia-takeaway--1&quot;&gt;Lo que esto cambia (Takeaway)&lt;/h3&gt;
&lt;p&gt;Un criterio de aceptación no es una opinión: es &lt;strong&gt;un test que puede fallar&lt;/strong&gt;. &quot;Tengo backup&quot; no significa nada hasta que has restaurado en una máquina limpia y has comprobado que el histórico y los informes siguen ahí. Y cuando el criterio se escribe como test, el diseño se ordena solo: aparece el manifiesto, aparece la copia consistente, aparecen los guardarraíles, y aparece el resto de la lista.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exportar es fácil. Recuperar es el trabajo.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;gracias&quot;&gt;Gracias&lt;/h3&gt;
&lt;p&gt;Este post no existiría sin el análisis de un amigo de la comunidad, que con IA asistida convirtió una intuición mía en una lista de requisitos verificables. Su pregunta —&quot;¿puedes restaurar en una máquina limpia?&quot;— fue el mejor &lt;em&gt;code review&lt;/em&gt; que ha recibido Aethernet. Gracias de verdad.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusi-n-objetiva&quot;&gt;Conclusión objetiva&lt;/h3&gt;
&lt;p&gt;La versión &lt;strong&gt;1.3.0&lt;/strong&gt; de Aethernet incorpora una recuperación offline demostrable: paquete con base consistente, configuración y manifiesto versionado; restore que valida, resguarda y migra; &lt;code&gt;aethernet restore --verify&lt;/code&gt; para comprobar una copia sin escribir; y un test que reproduce la prueba en una instalación limpia. Todo con &lt;code&gt;ruff&lt;/code&gt; + &lt;strong&gt;&lt;code&gt;mypy --strict&lt;/code&gt; (0 errores)&lt;/strong&gt; + &lt;code&gt;vulture&lt;/code&gt; + &lt;strong&gt;173 tests&lt;/strong&gt; en verde y CI sobre Python 3.11 y 3.12 (sin migración de esquema nueva: el esquema sigue en v5).&lt;/p&gt;
&lt;p&gt;No es una promesa de marketing: es un comando que puedes ejecutar.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aethernet backup                    # crea el paquete .tar.gz
aethernet restore --verify copia.tar.gz
aethernet restore copia.tar.gz      # recupérala en esta o en otra máquina
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;y-t--1&quot;&gt;Y tú&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;¿Cómo verificas hoy que tus copias de seguridad sirven?&lt;/strong&gt; ¿Alguna vez has restaurado un &quot;backup&quot; que no restauraba nada —y cómo lo descubriste? ¿Qué le exigirías a una herramienta para fiarte de su recuperación? Cuéntamelo en los comentarios; leo y respondo a todo.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;&lt;strong&gt;English version below&lt;/strong&gt; · El original está en español. If you're an English speaker, scroll down to the translation and tell me how you verify your backups in the discussion. I reply to everyone.&lt;/sub&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;hr&gt;
&lt;h2 id=&quot;your-backup-is-worthless-if-you-can-t-restore-it-the--wal-that-wiped-my-database&quot;&gt;Your backup is worthless if you can't restore it: the &lt;code&gt;-wal&lt;/code&gt; that wiped my database&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;em&gt;Auditable software&lt;/em&gt; — Post 02&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~10-12 · &lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;database&lt;/code&gt; &lt;code&gt;testing&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-confession-a-button-that-said-backup-and-guaranteed-nothing&quot;&gt;The confession: a button that said &quot;BACKUP&quot; and guaranteed nothing&lt;/h3&gt;
&lt;p&gt;There's a rule I learned the hard way: &lt;strong&gt;distrust your own features until you can prove them&lt;/strong&gt;. For a while, Aethernet had a big, green, reassuring button that said &lt;code&gt;BACKUP .TAR.GZ&lt;/code&gt;. You pressed it, a file came out, and you went to sleep feeling safe. It guaranteed absolutely nothing.&lt;/p&gt;
&lt;p&gt;That button is where this post begins. Not because it was broken —it did create a file— but because &lt;strong&gt;it looked like a promise and was really an untested hypothesis&lt;/strong&gt;: no one had ever restored that copy on another machine. I hadn't asked the only question that matters: does it work when you need it?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-feedback-that-put-it-in-writing&quot;&gt;The feedback that put it in writing&lt;/h3&gt;
&lt;p&gt;A friend from the community sent back an analysis —with an agentic model as co-pilot— that framed the problem better than I had. The core idea fits in one sentence: &lt;strong&gt;exporting is not recovering&lt;/strong&gt;. And he proposed a concrete acceptance criterion: an &lt;em&gt;offline&lt;/em&gt; recovery test on a clean machine, with no access to the original install, where the user can still inspect the history and generate a report.&lt;/p&gt;
&lt;p&gt;His exact words:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I'd add an offline recovery test: can a user restore a backup on a clean machine —with no access to the original install— and still inspect the history and generate a report? Export capability and recovery capability are distinct acceptance criteria.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He was right, and he also pointed at my project's exact gap: there was no documented recovery procedure covering the database, the configuration and the schema version, nor any clarity about which keys had to be handed over separately. That comment turned an intuition into a &lt;strong&gt;list of verifiable tasks&lt;/strong&gt;. The rest of this post is how I closed it. Thank you, &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/@lisarconnect&quot;&gt;&lt;code&gt;@lisarconnect&lt;/code&gt;&lt;/a&gt;, for the AI-assisted analysis.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-in-code&quot;&gt;The problem, in code&lt;/h3&gt;
&lt;p&gt;My backup was literally this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# The honest, wrong button
target = paths.data_dir / f&quot;aethernet-backup-{ts}.tar.gz&quot;
with tarfile.open(target, &quot;w:gz&quot;) as tar:
    tar.add(paths.db_path, arcname=&quot;aethernet.db&quot;)   # the database only
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four problems, and all four matter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It only copies the database.&lt;/strong&gt; The configuration (&lt;code&gt;my_ssids&lt;/code&gt;, aliases, retention…) is left out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It records no schema version.&lt;/strong&gt; On restore, you wouldn't know if the copy is older or newer than the app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It copies a live file.&lt;/strong&gt; The database runs in &lt;strong&gt;WAL&lt;/strong&gt; mode; copying the &lt;code&gt;.db&lt;/code&gt; by hand while writes are happening can give you an incomplete state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There's no restore.&lt;/strong&gt; A backup without a restore is an export with good manners.&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Export &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Recover&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Question &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;do the data read back? &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;do I boot up as if nothing happened?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Machine &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;this one, app installed &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;clean, with nothing from the original&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Test &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;open the report &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;restore and measure again&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-solution-in-four-pieces&quot;&gt;The solution, in four pieces&lt;/h3&gt;
&lt;h4 id=&quot;1-a-package-you-can-inspect&quot;&gt;1) A package you can inspect&lt;/h4&gt;
&lt;p&gt;A single &lt;code&gt;.tar.gz&lt;/code&gt; with three parts and a readable manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;app&quot;: &quot;aethernet&quot;,
  &quot;format_version&quot;: 1,
  &quot;app_version&quot;: &quot;1.3.0&quot;,
  &quot;schema_version&quot;: 5,
  &quot;created_at&quot;: 1759800000.0,
  &quot;db_sha256&quot;: &quot;9f2c…&quot;,
  &quot;members&quot;: [&quot;aethernet.db&quot;, &quot;config.toml&quot;, &quot;manifest.json&quot;],
  &quot;secrets_required&quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;secrets_required: []&lt;/code&gt; isn't decoration: it's a design statement. &lt;strong&gt;There are no irreplaceable keys to hand over.&lt;/strong&gt; The API token is regenerated and never travels in the package.&lt;/p&gt;
&lt;h4 id=&quot;2-a-consistent-copy-and-why-not-cp-&quot;&gt;2) A consistent copy (and why not &lt;code&gt;cp&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;With WAL active, copying the database file is a gamble. The right way is to let SQLite make the copy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;src = sqlite3.connect(str(db_path))
dst = sqlite3.connect(str(staged_db))
src.backup(dst)          # consistent snapshot, even with a live writer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Connection.backup()&lt;/code&gt; folds the WAL into a single coherent file. It's stdlib: no new dependencies.&lt;/p&gt;
&lt;h4 id=&quot;3-restore-without-breaking-anything&quot;&gt;3) Restore without breaking anything&lt;/h4&gt;
&lt;p&gt;Order matters, and every step exists because of a real failure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validate before writing&lt;/strong&gt; — manifest SHA-256, required members and safe paths (no &lt;code&gt;../&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reject the future&lt;/strong&gt; — if the copy uses a newer schema than the binary, it aborts with a clear message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect the present&lt;/strong&gt; — the current database is copied to &lt;code&gt;aethernet.db.bak-&amp;lt;date&amp;gt;&lt;/code&gt; before touching it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate forward&lt;/strong&gt; — migrations are &lt;em&gt;append-only&lt;/em&gt;, so an older copy upgrades itself on restore.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;4-the-criterion-turned-into-a-test&quot;&gt;4) The criterion turned into a test&lt;/h4&gt;
&lt;p&gt;This is the heart of it. The comment's acceptance criterion stopped being an opinion and became code that can fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_offline_recovery_on_clean_machine(tmp_path):
    # 1. origin: history + settings
    source = _paths(tmp_path, &quot;origin&quot;)
    ...  # scans and config are stored
    bundle = create_backup(source, tmp_path / &quot;backup.tar.gz&quot;)

    # 2. clean machine: another install, with nothing from the original
    clean = _paths(tmp_path, &quot;clean&quot;)
    restore_backup(bundle.path, clean)

    # 3. does it read the history and generate a report from scratch?
    repo = Repository(Database(clean.db_path))
    assert repo.wifi_scan_count() == 2
    report = build_report_data(repo, load_settings(clean))
    export(report, &quot;md&quot;, clean.report_dir / &quot;recovery.md&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this test goes red, my backup &lt;strong&gt;does not exist&lt;/strong&gt;. No interpretation: either it recovers, or it doesn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-bug-the--wal-that-wiped-my-restored-database&quot;&gt;The bug: the &lt;code&gt;-wal&lt;/code&gt; that wiped my restored database&lt;/h3&gt;
&lt;p&gt;The first version of restore passed the unit tests… and failed the real recovery one. It restored a copy with data and the database came up &lt;strong&gt;empty&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The root cause is a SQLite lesson I won't forget: when you replace &lt;code&gt;aethernet.db&lt;/code&gt; with &lt;code&gt;os.replace&lt;/code&gt;, the sidecar files &lt;code&gt;aethernet.db-wal&lt;/code&gt; and &lt;code&gt;aethernet.db-shm&lt;/code&gt; &lt;strong&gt;stay behind&lt;/strong&gt;. When SQLite reopens the restored database, it finds a WAL that belongs to &lt;em&gt;another&lt;/em&gt; database, &lt;strong&gt;replays it&lt;/strong&gt;, and overwrites your data with the previous state.&lt;/p&gt;
&lt;p&gt;The fix comes in two layers, because the failure had two doors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;_remove_sidecars(paths.db_path)          # drop the orphan -wal/-shm
os.replace(staged_db, paths.db_path)
_remove_sidecars(paths.db_path)          # and again, just in case
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…and in the CLI, &lt;strong&gt;closing the context connection before restoring&lt;/strong&gt;, because you don't restore over an open database. I added a test that restores &lt;em&gt;over a live WAL connection&lt;/em&gt; so it never happens again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;live = Database(paths.db_path); live.migrate()
restore_backup(bundle.path, paths)       # before: 0 scans. now: 1.
assert Repository(Database(paths.db_path)).wifi_scan_count() == 1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-other-bug-you-only-see-when-you-change-disks&quot;&gt;The other bug you only see when you change disks&lt;/h3&gt;
&lt;p&gt;With the test green, I tried another disk and hit &lt;code&gt;OSError: Invalid cross-device link&lt;/code&gt;. The &lt;em&gt;staging&lt;/em&gt; lived in &lt;code&gt;/tmp&lt;/code&gt; (often &lt;code&gt;tmpfs&lt;/code&gt;) and the final &lt;code&gt;os.replace&lt;/code&gt; pointed at &lt;code&gt;~/.local/share&lt;/code&gt;, on a different filesystem: a rename can't cross disks.&lt;/p&gt;
&lt;p&gt;The fix is a one-liner with real weight: &lt;strong&gt;staging lives next to the destination&lt;/strong&gt;, so the replacement is an atomic rename on the same filesystem. For the config, a &lt;code&gt;shutil.move&lt;/code&gt; (which does cross disks). I verified it with the database on &lt;code&gt;tmpfs&lt;/code&gt; and the temp on disk: recovery works.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-changes-takeaway--1&quot;&gt;What this changes (Takeaway)&lt;/h3&gt;
&lt;p&gt;An acceptance criterion isn't an opinion: it's &lt;strong&gt;a test that can fail&lt;/strong&gt;. &quot;I have a backup&quot; means nothing until you've restored it on a clean machine and checked that the history and the reports are still there. And once the criterion is written as a test, the design sorts itself out: the manifest appears, the consistent copy appears, the guardrails appear, and so does the rest of the list.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exporting is easy. Recovering is the job.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;thanks&quot;&gt;Thanks&lt;/h3&gt;
&lt;p&gt;This post wouldn't exist without the analysis from a friend in the community, who —with AI assistance— turned an intuition of mine into a list of verifiable requirements. His question —&quot;can you restore on a clean machine?&quot;— was the best code review Aethernet has ever received. &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/@lisarconnect&quot;&gt;&lt;code&gt;@lisarconnect&lt;/code&gt;&lt;/a&gt;, thank you, sincerely.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;objective-conclusion&quot;&gt;Objective conclusion&lt;/h3&gt;
&lt;p&gt;Version &lt;strong&gt;1.3.0&lt;/strong&gt; of Aethernet ships a demonstrable offline recovery: a package with a consistent database, the configuration and a versioned manifest; a restore that validates, protects and migrates; &lt;code&gt;aethernet restore --verify&lt;/code&gt; to check a copy without writing; and a test that reproduces the clean-machine trial. All of it with &lt;code&gt;ruff&lt;/code&gt; + &lt;strong&gt;&lt;code&gt;mypy --strict&lt;/code&gt; (0 errors)&lt;/strong&gt; + &lt;code&gt;vulture&lt;/code&gt; + &lt;strong&gt;173 tests&lt;/strong&gt; green and CI on Python 3.11 and 3.12 (no new schema migration: the schema is still v5).&lt;/p&gt;
&lt;p&gt;It isn't a marketing promise: it's a command you can run.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aethernet backup                    # create the .tar.gz package
aethernet restore --verify copy.tar.gz
aethernet restore copy.tar.gz       # recover it here or on another machine
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;over-to-you-1&quot;&gt;Over to you&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;How do you verify today that your backups actually work?&lt;/strong&gt; Have you ever restored a &quot;backup&quot; that restored nothing —and how did you find out? What would you demand from a tool before trusting its recovery? Tell me in the comments; I read and reply to everyone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;Project license: MIT · No secrets or keys in this post. · Projects: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion&quot;&gt;github.com/yosoyignicion&lt;/a&gt; · Aethernet code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion/Aethernet&quot;&gt;github.com/yosoyignicion/Aethernet&lt;/a&gt; · Release &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion/Aethernet/releases/tag/v1.3.0&quot;&gt;v1.3.0&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30129/tu-backup-no-sirve-si-no-puedes-restaurarlo-el-wal-que-vacio-mi-base</guid>
<pubDate>Wed, 07 Oct 2026 11:39:23 +0000</pubDate>
</item>
<item>
<title>Local-first no es una moda: es una decisión de arquitectura (4 criterios, caso real)</title>
<link>https://coderlegion.com/30039/local-first-no-es-una-moda-es-una-decision-de-arquitectura-4-criterios-caso-real</link>
<description>&amp;lt;!--
  CoderLegion · Article · Post 01
  Serie: Software auditable (S1)
  Autor: Ignacio Badenes (@yosoyignicion / IgnicionDev)
  Idioma: bilingüe ES + EN · Read time: ~8 min
  Tags: opensource, architecture, python, security
--&amp;gt;
&lt;h2 id=&quot;local-first-no-es-una-moda-es-una-decisi-n-de-arquitectura-4-criterios-caso-real-&quot;&gt;Local-first no es una moda: es una decisión de arquitectura (4 criterios, caso real)&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Serie:&lt;/strong&gt; &lt;em&gt;Software auditable&lt;/em&gt; — Post 01&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Etiquetas:&lt;/strong&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;architecture&lt;/code&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;security&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-esc-ner-de-red-que-sube-tus-ssid-a-la-nube-y-por-qu-eso-ya-es-una-decisi-n-de-arquitectura-&quot;&gt;El escáner de red que sube tus SSID a la nube (y por qué eso ya es una decisión de arquitectura)&lt;/h3&gt;
&lt;p&gt;Instala la típica app de &quot;analiza tu WiFi&quot; y lee la letra pequeña: cuenta obligatoria, sincronización en la nube, un SDK de analítica y, en el mejor de los casos, un interruptor de &quot;privacidad&quot; apagado por defecto. El problema no es que use la red. El problema es que &lt;strong&gt;no puedes auditar qué sale de tu equipo&lt;/strong&gt;, y sin eso no hay confianza: hay fe.&lt;/p&gt;
&lt;p&gt;Yo construí lo contrario. &lt;strong&gt;Aethernet&lt;/strong&gt; es un instrumento local para auditar tu WiFi doméstico: pasivo por defecto, sin nube, sin telemetría y honesto cuando su hardware no llega. Y lo que hace que sea &lt;em&gt;local-first&lt;/em&gt; no es un eslogan en la portada: es un conjunto de &lt;strong&gt;decisiones de arquitectura&lt;/strong&gt; que se pueden verificar. En este post las reduzco a &lt;strong&gt;cuatro criterios&lt;/strong&gt; que aplico antes de etiquetar cualquier proyecto como local-first. Si falla uno, no lo es.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;qui-n-escribe-esto&quot;&gt;Quién escribe esto&lt;/h3&gt;
&lt;p&gt;Soy Ignacio Badenes (&lt;strong&gt;IgnicionDev&lt;/strong&gt;), técnico de sistemas e informática aplicada desde Castellón. Vengo del soporte y del control de accesos, donde el registro y la mínima exposición no son filosofía: son el trabajo. Aethernet nació de ahí —auditar tu propia red sin regalársela a nadie— y es el primero de los proyectos que uso para explicar cómo diseño software que se puede leer, ejecutar y deshacer. Si esto te suena, &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/29882/mi-setup-de-agente-barato-opencode-deepseek-por-centimos-feat-igniciondev&quot;&gt;el post anterior sobre mi setup de agente barato&lt;/a&gt; es el contexto.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;la-tesis-sin-nube-es-f-cil-de-decir-y-caro-de-sostener&quot;&gt;La tesis: &quot;sin nube&quot; es fácil de decir y caro de sostener&lt;/h3&gt;
&lt;p&gt;Cualquiera puede escribir &lt;em&gt;privacy-first&lt;/em&gt; en una landing. Sostenerlo exige renunciar a atajos cómodos: el servicio que ya te da DNS, el CDN para las fuentes, el modelo en la nube para &quot;lo difícil&quot;, la nube como excusa para no diseñar degradación. El local-first no es un interruptor que se activa al final; es &lt;strong&gt;una restricción que se acepta al principio&lt;/strong&gt; y que condiciona cada capa.&lt;/p&gt;
&lt;p&gt;Mi definición operativa cabe en cuatro criterios. No son opiniones: son cosas que puedo comprobar en el repositorio.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;los-4-criterios-para-decidir-si-un-proyecto-puede-ser-local-first&quot;&gt;Los 4 criterios para decidir si un proyecto puede ser local-first&lt;/h3&gt;
&lt;h4 id=&quot;1-sin-red-saliente-no-outbound-&quot;&gt;1. Sin red saliente (no outbound)&lt;/h4&gt;
&lt;p&gt;La aplicación funciona sin conexión, no como &quot;modo demo&quot;: &lt;strong&gt;como modo normal&lt;/strong&gt;. Sin llamadas a servidores de terceros, sin autenticación remota, sin telemetría. En Aethernet, la propia interfaz sirve sus fuentes e iconos desde el disco (&lt;code&gt;/ae-fonts&lt;/code&gt;): ni siquiera pide una tipografía a Google. Si algo necesita la red, no es local-first; es cloud con buenos modales.&lt;/p&gt;
&lt;h4 id=&quot;2-sin-claves-ni-dependencia-de-un-proveedor&quot;&gt;2. Sin claves ni dependencia de un proveedor&lt;/h4&gt;
&lt;p&gt;Si para arrancar necesitas una &lt;code&gt;API_KEY&lt;/code&gt; o un registro, tu función principal es un servicio ajeno y tú solo eres el envoltorio. Local-first significa que &lt;strong&gt;el proyecto es dueño de su función&lt;/strong&gt;: Aethernet no pide ninguna clave, sus dependencias de ejecución están vacías y lo que aporta (medir, analizar, informar) ocurre en tu máquina.&lt;/p&gt;
&lt;h4 id=&quot;3-degradaci-n-elegante-y-honesta-&quot;&gt;3. Degradación elegante (y honesta)&lt;/h4&gt;
&lt;p&gt;Todo es opcional y el sistema sigue vivo cuando falta algo. Sin &lt;code&gt;iw&lt;/code&gt; no hay monitor mode; sin &lt;code&gt;scapy&lt;/code&gt; o &lt;code&gt;root&lt;/code&gt;, no hay ARP activo; sin &lt;code&gt;nmcli&lt;/code&gt;, no hay escaneo. En lugar de fallar en bloque, se baja de prestaciones y &lt;strong&gt;se dice la verdad&lt;/strong&gt;: si un dato no se puede medir, se marca &lt;code&gt;n/d&lt;/code&gt; en vez de inventarlo. La honestidad también es arquitectura.&lt;/p&gt;
&lt;h4 id=&quot;4-datos-tuyos-en-rutas-tuyas&quot;&gt;4. Datos tuyos, en rutas tuyas&lt;/h4&gt;
&lt;p&gt;Los datos viven en tu equipo, bajo tus rutas, en un formato que puedes inspeccionar. En Aethernet: config en &lt;code&gt;~/.config/aethernet/config.toml&lt;/code&gt;, base SQLite en &lt;code&gt;~/.local/share/aethernet/aethernet.db&lt;/code&gt; e informes exportables a Markdown, JSON, CSV o PDF. Sin secuestro de datos y sin lock-in.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-caso-real-c-mo-se-traduce-en-capas&quot;&gt;El caso real: cómo se traduce en capas&lt;/h3&gt;
&lt;p&gt;Aethernet no es un script con botones pegados. Es una arquitectura en capas donde &lt;strong&gt;CLI, API local y la interfaz &quot;AETHERNET&quot; consumen exactamente los mismos servicios&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Capa &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Paquete &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Responsabilidad&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Core &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.core&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Lógica pura y testeable: parseo &lt;code&gt;nmcli&lt;/code&gt;/&lt;code&gt;iw&lt;/code&gt;, ARP, reglas, salud, espectro, OUI.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Datos &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.data&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;SQLite con WAL, migraciones e histórico.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Servicio &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.service&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Daemon de vigilancia; &lt;strong&gt;nunca dibuja&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Alertas &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.alerts&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Cola, deduplicación, filtros y &lt;code&gt;notify-send&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Informes &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.report&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Exportadores Markdown / JSON / CSV / PDF.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;API &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;FastAPI local opcional, protegida por token.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;UI &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.ui&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;UI NiceGUI; solo lee la base y consume servicios.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Dos decisiones hacen que esto sea comprobable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Los parsers son funciones puras.&lt;/strong&gt; Se testean con texto real de &lt;code&gt;nmcli&lt;/code&gt; e &lt;code&gt;iw&lt;/code&gt; &lt;strong&gt;sin hardware&lt;/strong&gt;. Por eso puedo desarrollar en cualquier máquina y confiar en la CI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Las dependencias pesadas son extras opcionales.&lt;/strong&gt; El &lt;code&gt;pyproject.toml&lt;/code&gt; declara &lt;code&gt;dependencies = []&lt;/code&gt;; &lt;code&gt;scapy&lt;/code&gt;, &lt;code&gt;fastapi&lt;/code&gt;, &lt;code&gt;reportlab&lt;/code&gt;, &lt;code&gt;nicegui&lt;/code&gt; se instalan solo si los quieres (&lt;code&gt;pip install -e '.[lan,api,reports,ui]'&lt;/code&gt;). El núcleo arranca sin nada.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El flujo por defecto es &lt;strong&gt;pasivo&lt;/strong&gt;: se escucha sin inyectar tráfico ni cortar tu WiFi. Lo activo (ARP con &lt;code&gt;scapy&lt;/code&gt;) y la captura requieren &lt;code&gt;root&lt;/code&gt; y se habilitan a propósito. El propio proyecto te da un veredicto honesto de lo que tu equipo puede y no puede hacer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aethernet doctor        # veredicto honesto de tu hardware
aethernet scan --no-lan # escaneo pasivo
aethernet analyze       # motor de reglas sobre el último escaneo
aethernet networks --open
aethernet daemon start  # vigilancia continua
aethernet api           # API local opcional (token propio)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y el límite ético va escrito en la arquitectura, no en las FAQs: el modo activo está pensado para &lt;strong&gt;tu red&lt;/strong&gt;, y auditar redes ajenas sin permiso es, además de mala idea, ilegal.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-gate-de-calidad-por-qu-me-lo-creo-&quot;&gt;El gate de calidad (por qué me lo creo)&lt;/h3&gt;
&lt;p&gt;Local-first sin ingeniería es un render. Así que el proyecto pasa un gate duro antes de considerarse terminado: &lt;code&gt;ruff&lt;/code&gt; + &lt;strong&gt;&lt;code&gt;mypy --strict&lt;/code&gt; (0 errores)&lt;/strong&gt; + &lt;code&gt;vulture&lt;/code&gt; + &lt;code&gt;pytest&lt;/code&gt;, con &lt;strong&gt;más de 150 tests&lt;/strong&gt; y CI en GitHub Actions sobre Python &lt;strong&gt;3.11 y 3.12&lt;/strong&gt;. Además hay un smoke test E2E de la UI en modo headless sobre sus rutas principales.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./scripts/ci.sh      # ruff + mypy --strict + vulture + pytest
./scripts/smoke.sh   # E2E UI headless
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El tipado estricto no es decoración: los parsers reciben texto que puede venir de mil versiones de &lt;code&gt;nmcli&lt;/code&gt;, y el sistema de tipos me obliga a contemplar los casos raros antes de ejecutar. Menos fe, más contrato.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lo-honesto-qu-gano-y-qu-pierdo-con-local-first&quot;&gt;Lo honesto: qué gano y qué pierdo con local-first&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Gano&lt;/strong&gt; soberanía y auditabilidad: control total de mis datos, cero telemetría, cero lock-in y la tranquilidad de un sistema que puedo leer y deshacer. Puedo medir, exportar y borrar sin pedir permiso a nadie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pierdo&lt;/strong&gt; comodidad y techo de función: mantener parsers puros, degradación elegante y extras opcionales es &lt;strong&gt;más trabajo&lt;/strong&gt; que llamar a un servicio. Lo que en la nube sería una &lt;code&gt;fetch&lt;/code&gt; aquí es diseño de capas, tests y migraciones &lt;em&gt;append-only&lt;/em&gt;. Y hay funciones que solo tienen sentido con servidor (histórico agregado, análisis comparativo entre usuarios) y que renuncio a tener.&lt;/p&gt;
&lt;p&gt;Para mí el intercambio es ventajoso: prefiero un instrumento modesto que entiendo a una caja negra potente que no puedo auditar. Es la misma disciplina de fondo del resto de mi trabajo: si no lo puedo leer, ejecutar y probar, no me sirve.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;takeaway&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;El local-first no se declara en la landing; &lt;strong&gt;se demuestra en la arquitectura&lt;/strong&gt;. Sin red saliente, sin claves de terceros, con degradación elegante y con los datos en tus rutas. Si tu app no puede arrancar sin conexión, no es local-first: es nube con buenos modales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Y tú, ¿qué criterio añadirías para decidir si un proyecto puede ser local-first? ¿Cuál fue la última herramienta de red que abriste solo para ver a dónde enviaba tus datos?&lt;/strong&gt; Cuéntamelo en los comentarios; leo y respondo a todo.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;&lt;strong&gt;English version below&lt;/strong&gt; · El original está en español. Si eres angloparlante, baja a la traducción y cuéntame tu criterio en la discusión. Respondo a todos.&lt;/sub&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;hr&gt;
&lt;h2 id=&quot;local-first-isn-t-a-fad-it-s-an-architecture-decision-4-criteria-a-real-case-&quot;&gt;Local-first isn't a fad: it's an architecture decision (4 criteria, a real case)&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;em&gt;Auditable software&lt;/em&gt; — Post 01&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;architecture&lt;/code&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;security&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-network-scanner-that-uploads-your-ssids-to-the-cloud-and-why-that-s-already-an-architecture-decision-&quot;&gt;The network scanner that uploads your SSIDs to the cloud (and why that's already an architecture decision)&lt;/h3&gt;
&lt;p&gt;Install a typical &quot;analyze your WiFi&quot; app and read the fine print: mandatory account, cloud sync, an analytics SDK and, at best, a &quot;privacy&quot; toggle that ships off by default. The problem isn't that it uses the network. The problem is that &lt;strong&gt;you can't audit what leaves your machine&lt;/strong&gt;, and without that there is no trust: there is faith.&lt;/p&gt;
&lt;p&gt;I built the opposite. &lt;strong&gt;Aethernet&lt;/strong&gt; is a local instrument to audit your home WiFi: passive by default, no cloud, no telemetry and honest when its hardware can't keep up. And what makes it &lt;em&gt;local-first&lt;/em&gt; isn't a slogan on a landing page: it's a set of &lt;strong&gt;architecture decisions&lt;/strong&gt; you can verify. In this post I reduce them to &lt;strong&gt;four criteria&lt;/strong&gt; I apply before labelling any project local-first. If one fails, it isn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;who-s-writing-this&quot;&gt;Who's writing this&lt;/h3&gt;
&lt;p&gt;I'm Ignacio Badenes (&lt;strong&gt;IgnicionDev&lt;/strong&gt;), an IT technician from Castellón, Spain. I come from support and access control, where logging and minimal exposure aren't philosophy: they're the job. Aethernet was born there —auditing your own network without giving it away to anyone— and it's the first of the projects I use to explain how I design software you can read, run and undo. If that resonates, &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/29882/mi-setup-de-agente-barato-opencode-deepseek-por-centimos-feat-igniciondev&quot;&gt;my previous post about my cheap agent setup&lt;/a&gt; is the context.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-thesis-no-cloud-is-easy-to-say-and-expensive-to-sustain&quot;&gt;The thesis: &quot;no cloud&quot; is easy to say and expensive to sustain&lt;/h3&gt;
&lt;p&gt;Anyone can write &lt;em&gt;privacy-first&lt;/em&gt; on a landing page. Sustaining it means giving up convenient shortcuts: the service that gives you DNS, the CDN for fonts, the cloud model for &quot;the hard part&quot;, the cloud as an excuse not to design for degradation. Local-first isn't a switch you flip at the end; it's &lt;strong&gt;a constraint you accept at the beginning&lt;/strong&gt; that shapes every layer.&lt;/p&gt;
&lt;p&gt;My working definition fits in four criteria. They aren't opinions: they're things I can check in the repository.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-4-criteria-to-decide-whether-a-project-can-be-local-first&quot;&gt;The 4 criteria to decide whether a project can be local-first&lt;/h3&gt;
&lt;h4 id=&quot;1-no-outbound-network&quot;&gt;1. No outbound network&lt;/h4&gt;
&lt;p&gt;The application works offline, not as a &quot;demo mode&quot;: &lt;strong&gt;as the normal mode&lt;/strong&gt;. No calls to third-party servers, no remote auth, no telemetry. In Aethernet the UI itself serves its fonts and icons from disk (&lt;code&gt;/ae-fonts&lt;/code&gt;): it doesn't even request a typeface from Google. If something needs the network, it isn't local-first; it's cloud with good manners.&lt;/p&gt;
&lt;h4 id=&quot;2-no-keys-and-no-provider-dependency&quot;&gt;2. No keys and no provider dependency&lt;/h4&gt;
&lt;p&gt;If starting up requires an &lt;code&gt;API_KEY&lt;/code&gt; or a sign-up, your core function is someone else's service and you're just the wrapper. Local-first means &lt;strong&gt;the project owns its function&lt;/strong&gt;: Aethernet asks for no key, its runtime dependencies are empty, and what it delivers (measure, analyze, report) happens on your machine.&lt;/p&gt;
&lt;h4 id=&quot;3-graceful-degradation-and-honesty-&quot;&gt;3. Graceful degradation (and honesty)&lt;/h4&gt;
&lt;p&gt;Everything is optional and the system stays alive when something is missing. Without &lt;code&gt;iw&lt;/code&gt; there's no monitor mode; without &lt;code&gt;scapy&lt;/code&gt; or &lt;code&gt;root&lt;/code&gt;, no active ARP; without &lt;code&gt;nmcli&lt;/code&gt;, no scan. Instead of failing as a whole, it scales back and &lt;strong&gt;tells the truth&lt;/strong&gt;: if a value can't be measured, it's marked &lt;code&gt;n/d&lt;/code&gt; instead of invented. Honesty is architecture too.&lt;/p&gt;
&lt;h4 id=&quot;4-your-data-in-your-paths&quot;&gt;4. Your data, in your paths&lt;/h4&gt;
&lt;p&gt;Data lives on your machine, under your paths, in a format you can inspect. In Aethernet: config in &lt;code&gt;~/.config/aethernet/config.toml&lt;/code&gt;, SQLite database in &lt;code&gt;~/.local/share/aethernet/aethernet.db&lt;/code&gt; and reports exportable to Markdown, JSON, CSV or PDF. No data hostage-taking and no lock-in.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-case-what-it-looks-like-in-layers&quot;&gt;The real case: what it looks like in layers&lt;/h3&gt;
&lt;p&gt;Aethernet isn't a script with glued-on buttons. It's a layered architecture where &lt;strong&gt;the CLI, the local API and the &quot;AETHERNET&quot; UI consume exactly the same services&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Layer &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Package &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Core &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.core&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Pure, testable logic: &lt;code&gt;nmcli&lt;/code&gt;/&lt;code&gt;iw&lt;/code&gt; parsing, ARP, rules, health, spectrum, OUI.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Data &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.data&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;SQLite with WAL, migrations and history.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Service &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.service&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Watch daemon; &lt;strong&gt;never draws&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Alerts &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.alerts&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Queue, dedup, filters and &lt;code&gt;notify-send&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Reports &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.report&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Markdown / JSON / CSV / PDF exporters.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;API &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Optional local FastAPI, token-protected.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;UI &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;aethernet.ui&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;NiceGUI UI; only reads the DB and consumes services.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two decisions make this verifiable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parsers are pure functions.&lt;/strong&gt; They're tested against real &lt;code&gt;nmcli&lt;/code&gt; and &lt;code&gt;iw&lt;/code&gt; output &lt;strong&gt;without hardware&lt;/strong&gt;. That's why I can develop on any machine and trust CI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heavy dependencies are optional extras.&lt;/strong&gt; &lt;code&gt;pyproject.toml&lt;/code&gt; declares &lt;code&gt;dependencies = []&lt;/code&gt;; &lt;code&gt;scapy&lt;/code&gt;, &lt;code&gt;fastapi&lt;/code&gt;, &lt;code&gt;reportlab&lt;/code&gt;, &lt;code&gt;nicegui&lt;/code&gt; are installed only if you want them (&lt;code&gt;pip install -e '.[lan,api,reports,ui]'&lt;/code&gt;). The core runs with nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The default flow is &lt;strong&gt;passive&lt;/strong&gt;: it listens without injecting traffic or cutting your WiFi. Active ARP scanning (&lt;code&gt;scapy&lt;/code&gt;) and capture require &lt;code&gt;root&lt;/code&gt; and are enabled on purpose. The project itself gives you an honest verdict of what your gear can and can't do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aethernet doctor        # honest verdict of your hardware
aethernet scan --no-lan # passive scan
aethernet analyze       # rule engine over the last scan
aethernet networks --open
aethernet daemon start  # continuous watch
aethernet api           # optional local API (its own token)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the ethical limit is written into the architecture, not the FAQs: active mode targets &lt;strong&gt;your network&lt;/strong&gt;, and auditing networks you don't own is, besides a bad idea, illegal.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-quality-gate-why-i-trust-it-&quot;&gt;The quality gate (why I trust it)&lt;/h3&gt;
&lt;p&gt;Local-first without engineering is a mockup. So the project passes a hard gate before it counts as done: &lt;code&gt;ruff&lt;/code&gt; + &lt;strong&gt;&lt;code&gt;mypy --strict&lt;/code&gt; (0 errors)&lt;/strong&gt; + &lt;code&gt;vulture&lt;/code&gt; + &lt;code&gt;pytest&lt;/code&gt;, with &lt;strong&gt;150+ tests&lt;/strong&gt; and GitHub Actions CI on Python &lt;strong&gt;3.11 and 3.12&lt;/strong&gt;. There's also a headless end-to-end UI smoke test over its main routes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./scripts/ci.sh      # ruff + mypy --strict + vulture + pytest
./scripts/smoke.sh   # headless E2E UI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Strict typing isn't decoration: the parsers take text that may come from a thousand versions of &lt;code&gt;nmcli&lt;/code&gt;, and the type system forces me to handle the odd cases before running anything. Less faith, more contract.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-honest-part-what-i-gain-and-what-i-lose-with-local-first&quot;&gt;The honest part: what I gain and what I lose with local-first&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;I gain&lt;/strong&gt; sovereignty and auditability: full control of my data, zero telemetry, zero lock-in and the peace of mind of a system I can read and undo. I can measure, export and delete without asking anyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I lose&lt;/strong&gt; convenience and a feature ceiling: keeping pure parsers, graceful degradation and optional extras is &lt;strong&gt;more work&lt;/strong&gt; than calling a service. What would be a &lt;code&gt;fetch&lt;/code&gt; in the cloud is here layer design, tests and &lt;em&gt;append-only&lt;/em&gt; migrations. And some features only make sense with a server (aggregated history, cross-user comparison) and I give them up.&lt;/p&gt;
&lt;p&gt;For me the trade is worth it: I'd rather have a modest instrument I understand than a powerful black box I can't audit. It's the same underlying discipline as the rest of my work: if I can't read it, run it and test it, it's no good to me.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;takeaway-1&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;Local-first isn't declared on a landing page; &lt;strong&gt;it's proven in the architecture&lt;/strong&gt;. No outbound network, no third-party keys, graceful degradation and your data in your paths. If your app can't boot without a connection, it isn't local-first: it's cloud with good manners.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So what about you? Which criterion would you add to decide whether a project can be local-first? What was the last network tool you opened just to see where it was sending your data?&lt;/strong&gt; Tell me in the comments; I read and reply to everyone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;Licencia de los proyectos mencionados: MIT · Sin secretos ni claves en este post. · Proyectos: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion&quot;&gt;github.com/yosoyignicion&lt;/a&gt; · Código de Aethernet: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion/Aethernet&quot;&gt;github.com/yosoyignicion/Aethernet&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30039/local-first-no-es-una-moda-es-una-decision-de-arquitectura-4-criterios-caso-real</guid>
<pubDate>Tue, 06 Oct 2026 17:42:26 +0000</pubDate>
</item>
<item>
<title>We Promise Transparency. Not the &quot;Liquid Glass&quot; Kind.</title>
<link>https://coderlegion.com/29977/we-promise-transparency-not-the-liquid-glass-kind</link>
<description>&lt;p&gt;Let's get the joke out of the way first, because I can already see someone's fingers hovering over the comment box: no, when we talk about &quot;transparency&quot; here, we are not launching a translucent UI skin with rounded corners and a marketing deck about &quot;depth&quot; and &quot;light.&quot; Nobody's redesigning your icons to look like they're floating in a glass of water. Apple can keep that one. Transparency, as we mean it, isn't about making things look like glass. It's about making sure there's nothing opaque left to hide behind in the first place.&lt;/p&gt;
&lt;h3 id=&quot;transparency-is-the-rarest-feature-in-modern-software&quot;&gt;Transparency Is the Rarest Feature in Modern Software&lt;/h3&gt;
&lt;p&gt;Here's the uncomfortable observation that started all of this: almost nothing you run today will actually tell you what it's doing. You call a function, and underneath it, a dozen layers you never asked for quietly make decisions on your behalf — a runtime schedules something, an allocator decides where your data actually lives, a framework &quot;helpfully&quot; retries a failed call you didn't know could fail, a standard library wraps three other standard libraries you've never heard of. Most of the time that's a perfectly reasonable trade. Most software genuinely benefits from not making every engineer re-derive memory management from first principles on a Tuesday.&lt;/p&gt;
&lt;p&gt;But somewhere along the way, &quot;we abstracted this for your convenience&quot; quietly turned into &quot;we abstracted this so thoroughly that nobody on the team, including us, can tell you with certainty what your program actually does when it touches the kernel.&quot; That's not convenience anymore. That's a black box with a changelog. And an entire generation of developers has been trained to treat &quot;it works, don't ask why&quot; as an acceptable final answer, because asking why usually means descending through four more layers of someone else's unexamined assumptions.&lt;/p&gt;
&lt;p&gt;Developer transparency, the version we actually care about, is the opposite stance, stated plainly: no hidden magic, no invisible runtime quietly squatting on your CPU cycles like a tenant you never agreed to lease to, and no stack of &quot;convenience&quot; layers standing between your code and the hardware that's supposed to be running it. You should be able to point at any behaviour your program exhibits and trace it to a decision — yours, specifically — not to an abstraction that made the call for you somewhere you'll never see.&lt;/p&gt;
&lt;p&gt;This is where Gloria comes in, because it's the sharpest, most literal expression of that idea we've built so far.&lt;/p&gt;
&lt;h3 id=&quot;what-transparency-actually-looks-like-in-gloria&quot;&gt;What Transparency Actually Looks Like in Gloria&lt;/h3&gt;
&lt;p&gt;When you write something in Gloria, you know exactly what instruction goes to the CPU, exactly what memory gets mapped, and exactly which register is holding your data at any given moment. You are not down on one knee, hat in hand, begging a heavyweight framework or a sprawling C standard library to please do the thing for you. You own the execution, start to finish, with nobody else's fingerprints on it.&lt;/p&gt;
&lt;p&gt;From a Linux perspective, Gloria treats the kernel as its one and only direct interface. No linking against &lt;code&gt;glibc&lt;/code&gt;. No standard C startup routine quietly initializing half a universe of state before your &lt;code&gt;main&lt;/code&gt; function has even had the decency to run. Gloria generates self-contained, raw ELF64 binaries for AMD64, and that's genuinely all that's in there.&lt;/p&gt;
&lt;p&gt;When your code needs to open a socket, allocate memory, or spin up a bit of lightweight concurrency, it doesn't go politely knocking on some third-party wrapper function's door and waiting to be let in. It loads the syscall number straight into &lt;code&gt;RAX&lt;/code&gt;, lays out the arguments exactly per the System V AMD64 ABI, and hits the kernel directly with a &lt;code&gt;syscall&lt;/code&gt; instruction. No chaperone. No translation layer quietly editorializing your request on the way through. The resulting binaries stay under &lt;strong&gt;9KB&lt;/strong&gt;, because there is precisely zero runtime bloat riding along for the trip. Put that next to the multi-megabyte &quot;hello world&quot; some modern frameworks ship by default, and you start to get an honest sense of exactly how much weight we've actually cut.&lt;/p&gt;
&lt;h3 id=&quot;why-this-needed-to-exist-at-all&quot;&gt;Why This Needed to Exist at All&lt;/h3&gt;
&lt;p&gt;Here's the pitch, cynicism included at no extra charge: modern systems engineering has gone soft. Not incompetent — soft. Comfortable. Padded on every side with abstractions that exist specifically to protect you from having to understand what your own program is doing. Most software doesn't need to care which register holds a value. This project exists for the software that does, and for the engineers who'd rather know than be protected from knowing.&lt;/p&gt;
&lt;p&gt;We wanted something that gives you the raw control and speed of C, the type expressiveness and memory-safety thinking of Rust, and the dead-simple concurrency model of Go — minus the overhead each of those languages still quietly carries around. This is built for the specific kind of developer who needs to wring every last drop of performance out of a box, who's writing ultra-fast microservices, custom network infrastructure, or logic that has to survive on genuinely minimal resources, where every kilobyte and every CPU cycle is a line item somebody's actually tracking, not an abstraction nobody bothers to measure.&lt;/p&gt;
&lt;h3 id=&quot;the-roadmap-stated-without-the-usual-hedging&quot;&gt;The Roadmap, Stated Without the Usual Hedging&lt;/h3&gt;
&lt;p&gt;Our self-hosting plan is straightforward and, I'll admit, a little uncompromising. Right now, the ForgeZero compiler, &lt;code&gt;fz&lt;/code&gt;, is written in Go. That's a deliberate choice, not a compromise we're quietly hoping nobody notices — Go let us bootstrap the language fast, iterate on syntax without a chicken-and-egg problem slowing every decision down, and shake the remaining edge cases out of the lexer, parser, and semantic checker while the ground was still allowed to move under our feet.&lt;/p&gt;
&lt;p&gt;But Go is a bridge, not a destination. The actual milestone the entire Gloria ecosystem is being built toward is full self-hosting. We're building out the language constructs, inline assembly support, and memory primitives specifically so that Gloria can eventually compile its own compiler, from scratch, with nothing borrowed. The day &lt;code&gt;fz&lt;/code&gt; is rewritten in pure Gloria is the day we cut the last external dependency loose entirely — and the day this architecture stops being &quot;an interesting idea with training wheels&quot; and starts being something you can point at and say, flatly, that it's ready for serious production.&lt;/p&gt;
&lt;p&gt;That's the whole philosophy, really, scaled up from one register to an entire compiler: nothing in this stack should exist that we can't fully explain, and nothing should run that isn't accountable to someone who actually understands it.&lt;/p&gt;
&lt;h3 id=&quot;watch-it-happen-instead-of-reading-about-it-later&quot;&gt;Watch It Happen Instead of Reading About It Later&lt;/h3&gt;
&lt;p&gt;If any of this is the kind of problem that genuinely interests you — not the polished retrospective six months from now, but the actual process, bugs included — we post it all in close to real time on our Telegram channel, &lt;strong&gt;@forgezeroteam&lt;/strong&gt;. Build updates, broken things, fixed things, syntax decisions argued out loud before they're final. It's faster and considerably more honest than waiting for the cleaned-up version to show up here.&lt;/p&gt;
&lt;p&gt;No liquid glass. No rounded corners pretending to be depth. Just a binary you can actually read, a register you can actually name, and a compiler that, one day soon, won't owe its existence to anything but itself.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/29977/we-promise-transparency-not-the-liquid-glass-kind</guid>
<pubDate>Tue, 06 Oct 2026 11:10:46 +0000</pubDate>
</item>
<item>
<title>ForgeZero and Gloria: A Honest Look at a New Systems Language</title>
<link>https://coderlegion.com/29895/forgezero-and-gloria-a-honest-look-at-a-new-systems-language</link>
<description>&lt;p&gt;Hi everyone. In this article I want to talk about ForgeZero and their own programming language, Gloria. I'll keep the review honest, with no exaggeration in either direction.&lt;br&gt;
Before getting into it, I want to say a separate thank you to my mentor and teacher, Alex Voste, who introduced me to platforms like CoderLegion and has given me an enormous amount of knowledge along the way. It's largely thanks to him that I learned Linux properly and finally settled on my path in IT.&lt;br&gt;
Why this project stands out&lt;br&gt;
If you assumed that building your own systems language in 2026 is either a romantic exercise or a waste of time, ForgeZero is worth a second look. Gloria isn't another toy DSL bolted on top of Python. It's a serious, fully self-hosted compiler that emits native AMD64 binaries only a few hundred bytes in size, with no dependency on a Go runtime or libc.&lt;br&gt;
What actually caught my attention&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Zero-allocation by default.&lt;br&gt;
There's no bloated heap sitting behind the scenes. Gloria talks to the kernel directly through syscalls (mmap, sys_open, write), which gives you tight, explicit control over memory from the very first line.&lt;/li&gt;
&lt;li&gt;Deliberate architectural choices.&lt;br&gt;
A few design decisions here show real engineering taste: &amp;gt;&amp;gt; chaining with register passing, built-in |? guard operators for safe short-circuiting, native vectors implemented with REP MOVSQ, and hash tables built on open addressing. None of this feels bolted on — it reads like a language designed around how the hardware actually works.&lt;/li&gt;
&lt;li&gt;Coroutines written in pure assembly.&lt;br&gt;
The project natively supports cooperative green threads (fibers), complete with its own register-level context switching and 4KB mmap-backed stacks. That's not a small feat to get right, and it shows a level of commitment most &quot;systems-adjacent&quot; languages don't bother with.&lt;br&gt;
My verdict&lt;br&gt;
ForgeZero and Gloria represent a genuinely substantial piece of work for anyone who still cares about true bare-metal systems programming. The language addresses a real pain point for engineers: it offers a clean, readable syntax while preserving full hardware predictability and extremely compact binaries. You don't often get both at once.&lt;br&gt;
This is a project worth following for anyone who misses the days when code was written for speed, not for how many gigabytes of RAM it could comfortably consume.&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/29895/forgezero-and-gloria-a-honest-look-at-a-new-systems-language</guid>
<pubDate>Mon, 05 Oct 2026 19:59:23 +0000</pubDate>
</item>
<item>
<title>Mi setup de agente barato: OpenCode + DeepSeek por céntimos (feat. IgnicionDev)</title>
<link>https://coderlegion.com/29882/mi-setup-de-agente-barato-opencode-deepseek-por-centimos-feat-igniciondev</link>
<description>&amp;lt;!--
  CoderLegion · Article · Post 00
  Serie: My cheap Setup feat IgnicionDev
  Autor: Ignacio Badenes (@yosoyignicion / IgnicionDev)
  Idioma: bilingüe ES + EN · Read time: ~8 min
  Tags: ai, aiagents, devtools, mcp · Extra: linux, opensource
--&amp;gt;
&lt;h2 id=&quot;mi-setup-de-agente-barato-opencode-deepseek-por-c-ntimos-feat-igniciondev-&quot;&gt;Mi setup de agente barato: OpenCode + DeepSeek por céntimos (feat. IgnicionDev)&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Serie:&lt;/strong&gt; &lt;em&gt;My cheap Setup feat IgnicionDev&lt;/em&gt; — Post 00&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Etiquetas:&lt;/strong&gt; &lt;code&gt;ai&lt;/code&gt; &lt;code&gt;aiagents&lt;/code&gt; &lt;code&gt;devtools&lt;/code&gt; &lt;code&gt;mcp&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;abr-un-agente-que-cuesta-c-ntimos-y-me-devolvi-control-no-ruido&quot;&gt;Abrí un agente que cuesta céntimos y me devolvió control, no ruido&lt;/h3&gt;
&lt;p&gt;La mayoría de la conversación sobre agentes de código empieza por la potencia: el modelo más grande, el benchmark más alto, la demo más vistosa. Mi problema era el contrario. Yo no necesitaba &lt;em&gt;más&lt;/em&gt; inteligencia: necesitaba &lt;strong&gt;algo que costara céntimos, que no inundara la terminal y cuyo resultado pudiera auditar línea a línea&lt;/strong&gt;. Si no puedo leer y ejecutar lo que el agente produce, no me sirve: trabajo con dinero, facturas y datos que deben cuadrar.&lt;/p&gt;
&lt;p&gt;Así que me monté un agente barato —&lt;strong&gt;OpenCode + DeepSeek Flash&lt;/strong&gt;— y lo rodeé de disciplina: memoria persistente, reglas de gasto de tokens, permisos estrictos y verificaciones reales. Este post es el &quot;cómo&quot;, con números, y termina con &lt;strong&gt;diez cosas concretas&lt;/strong&gt; que puedes hacer hoy con un setup así. No es magia; es ingeniería honesta.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;qui-n-escribe-esto-1&quot;&gt;Quién escribe esto&lt;/h3&gt;
&lt;p&gt;Soy Ignacio Badenes (&lt;strong&gt;IgnicionDev&lt;/strong&gt;), técnico de sistemas e informática aplicada desde Castellón, España. Vengo del soporte y del control de accesos: protocolos, registros, sangre fría cuando algo falla. Ese hábito lo aplico ahora al software que construyo y publico —&lt;strong&gt;Aethernet&lt;/strong&gt; (auditoría WiFi local, 146 tests), &lt;strong&gt;LocalGrant-Finder&lt;/strong&gt; (subvenciones del BOE con búsqueda híbrida), &lt;strong&gt;SystemBridge-AI&lt;/strong&gt; (facturas a libro contable, dinero en &lt;code&gt;Decimal&lt;/code&gt;) y &lt;strong&gt;AssetShrink&lt;/strong&gt; (C++17, imágenes en local)—. Todos con tests, CI y licencia. Si una herramienta no es auditable, no la publico. Con mi agente, igual.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;la-tesis-la-disciplina-sustituye-a-la-potencia&quot;&gt;La tesis: la disciplina sustituye a la potencia&lt;/h3&gt;
&lt;p&gt;Un modelo barato &lt;strong&gt;no&lt;/strong&gt; te obliga a obtener resultados baratos. Te obliga a dejar de improvisar. El truco no está en el modelo: está en &lt;strong&gt;gastar pocos tokens sin perder precisión&lt;/strong&gt;. En DeepSeek Flash el precio se reparte así:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;coste = input_fresco × $0,14/M  +  input_cacheado × $0,0028/M  +  output × $0,28/M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El input cacheado cuesta &lt;strong&gt;50 veces menos&lt;/strong&gt; que el fresco, y el output es la partida más cara. Por eso ataqué los tres frentes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;System prompt y herramientas mínimas&lt;/strong&gt; (menos tokens fijos por petición).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Máximo reúso de caché&lt;/strong&gt; (mismo prefijo, sin UUIDs aleatorios que la rompen).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output controlado&lt;/strong&gt; (respuestas cortas o JSON, nada de párrafos de relleno).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En mi escenario de trabajo, esto reduce el gasto ~&lt;strong&gt;82 %&lt;/strong&gt;; una vez la caché está caliente, el coste marginal por petición cae a una &lt;strong&gt;fracción de céntimo&lt;/strong&gt;. No es &quot;gratis&quot;, pero se acerca. Y lo importante: sigue siendo correcto porque &lt;strong&gt;yo pongo la disciplina&lt;/strong&gt; donde el modelo no llega.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;el-setup-opencode-deepseek&quot;&gt;El setup: OpenCode + DeepSeek&lt;/h3&gt;
&lt;p&gt;Mi modelo es &lt;code&gt;deepseek/deepseek-v4-flash&lt;/code&gt;, servido por un proveedor compatible con OpenAI y con la caché activada:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &quot;model&quot;: &quot;deepseek/deepseek-v4-flash&quot;,
  &quot;provider&quot;: {
    &quot;deepseek&quot;: {
      &quot;npm&quot;: &quot;@ai-sdk/openai-compatible&quot;,
      &quot;options&quot;: { &quot;setCacheKey&quot;: true },
      &quot;models&quot;: {
        &quot;deepseek-v4-flash&quot;: {
          &quot;reasoning&quot;: true,
          &quot;interleaved&quot;: { &quot;field&quot;: &quot;reasoning_content&quot; }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tres decisiones que marcan la diferencia a lo largo del día:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compactación automática&lt;/strong&gt; (&lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;prune&lt;/code&gt;, &lt;code&gt;reserved: 5000&lt;/code&gt;): la conversación se poda sola y no arrastro historial gigante.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Límite de salida de herramientas&lt;/strong&gt; (&lt;code&gt;max_lines: 200&lt;/code&gt;, &lt;code&gt;max_bytes: 8192&lt;/code&gt;): un &lt;code&gt;npm install&lt;/code&gt; no me escupe 200 líneas a la cara; si trunca, reejecuto acotado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permisos estrictos&lt;/strong&gt; (&lt;code&gt;webfetch: deny&lt;/code&gt;, plan en solo lectura): el agente no navega ni escribe donde no debe sin que yo lo apruebe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sobre todo esto manda un archivo: mi &lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt;. No es una lista de deseos, es un &lt;strong&gt;router de trabajo&lt;/strong&gt; con protocolo (entender → planear → cambio pequeño → verificar → revisar), una &lt;em&gt;Definition of Done&lt;/em&gt; honesta (typecheck, tests, lint, diff revisado) y reglas de gasto. El modelo es barato; el criterio no.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;memoria-y-contexto-que-no-repita-lo-que-ya-sabe&quot;&gt;Memoria y contexto: que no repita lo que ya sabe&lt;/h3&gt;
&lt;p&gt;Un agente sin memoria es un becario con amnesia: cada sesión empieza de cero y vuelve a pagar por lo mismo. Yo uso el plugin &lt;strong&gt;&lt;code&gt;engram + zerotoken&lt;/code&gt;&lt;/strong&gt;: memoria tipada en SQLite con &lt;strong&gt;FTS5&lt;/strong&gt;, &lt;strong&gt;auto-guardado&lt;/strong&gt; de errores→solución, decisiones de arquitectura y compactaciones, e &lt;strong&gt;inyección de solo 2 memorias / 600 tokens&lt;/strong&gt; en cada arranque. Traducido: en vez de arrastrar 50 000 tokens de historial, inyecto lo justo.&lt;/p&gt;
&lt;p&gt;A eso le sumo &lt;strong&gt;Context7&lt;/strong&gt;: cuando pregunto por una API o versión concreta, trae la documentación real y &lt;strong&gt;la cacheo como memoria&lt;/strong&gt;. La próxima vez no vuelvo a consultarla ni el modelo la alucina. Documentación viva, cero repetición.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-opencode-conf-el-proyecto-privado-que-da-forma-a-todo&quot;&gt;My.OpenCode.Conf: el proyecto privado que da forma a todo&lt;/h3&gt;
&lt;p&gt;Aquí está la pieza que mucha gente no ve. Mi configuración no es un &lt;code&gt;opencode.json&lt;/code&gt; suelto: es un proyecto de GitHub &lt;strong&gt;privado&lt;/strong&gt; llamado &lt;strong&gt;My.OpenCode.Conf&lt;/strong&gt;, parte de mi aprendizaje y mi forma de trabajar. Es un &lt;em&gt;bundle&lt;/em&gt; de &lt;strong&gt;tres subproyectos independientes&lt;/strong&gt; que se hablan por un protocolo abierto (&lt;strong&gt;MCP&lt;/strong&gt;):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Subproyecto &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Qué aporta &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Hecho con&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;skill-matrix&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;+240 skills en formato &lt;code&gt;SKILL.md&lt;/code&gt; (técnicas y de diseño) que el agente carga &lt;strong&gt;bajo demanda&lt;/strong&gt; por triggers &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Markdown&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;second-termux-v2&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Servidor MCP que lanza &lt;strong&gt;jobs en background&lt;/strong&gt; y devuelve &lt;strong&gt;una sola línea&lt;/strong&gt; de estado en vez de inundar la TUI &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;TypeScript (ESM)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;engram+zerotoken&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;El plugin de &lt;strong&gt;memoria persistente&lt;/strong&gt; (FTS5) y la política de ahorro de tokens &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;TypeScript (Bun)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Los une un &lt;strong&gt;prompt maestro&lt;/strong&gt;: en vez de mantener un &lt;code&gt;install.sh&lt;/code&gt; frágil, &lt;em&gt;el propio agente se autoconfigura&lt;/em&gt; (detecta el sistema operativo y adapta los comandos). Y, para mí, lo mejor de su diseño: es &lt;strong&gt;reversible&lt;/strong&gt;. Se desinstala limpio, sin lock-in, sin telemetría y sin tocar lo que ya tenías. El repo es privado por ahora —lo enseño, lo explico y respondo dudas—, pero la lección es pública: &lt;strong&gt;una buena config es código que se puede leer, verificar y deshacer.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;las-herramientas-que-lo-conectan-mcp-y-plugins-&quot;&gt;Las herramientas que lo conectan (MCP y plugins)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;chrome-devtools&lt;/code&gt; (headless)&lt;/strong&gt; — un navegador real que el agente maneja: navegar, extraer, y hacer auditorías &lt;strong&gt;Lighthouse&lt;/strong&gt; sin abrir una ventana.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;github&lt;/code&gt;&lt;/strong&gt; — issues, PRs y acciones desde la propia conversación.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sqlite&lt;/code&gt;&lt;/strong&gt; — consultas sobre mi base de datos local de trabajo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;context7&lt;/code&gt;&lt;/strong&gt; — documentación de librerías en vivo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;human-followup.ts&lt;/code&gt;&lt;/strong&gt; — plugin propio que usa &lt;code&gt;notify-send&lt;/code&gt;: cuando el agente &lt;strong&gt;pide datos&lt;/strong&gt;, necesita un &lt;strong&gt;permiso&lt;/strong&gt; o &lt;strong&gt;termina&lt;/strong&gt; una navegación, me salta una notificación de escritorio. Puedo dejar de mirar la terminal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagentes&lt;/strong&gt; (&lt;code&gt;explore&lt;/code&gt;, &lt;code&gt;general&lt;/code&gt;) — para repartir exploración y trabajo pesado sin contaminar mi contexto.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;crear-tu-propio-mcp-para-publicar-sin-salir-del-agente&quot;&gt;Crear tu propio MCP para publicar sin salir del agente&lt;/h3&gt;
&lt;p&gt;El ejemplo más honesto de &quot;herramienta propia&quot; es este mismo post. En vez de copiar y pegar texto en el navegador, escribí un &lt;strong&gt;MCP local&lt;/strong&gt; para Coder Legion en &lt;strong&gt;Node ESM sin dependencias&lt;/strong&gt; (~300 líneas): una conversación &lt;strong&gt;JSON-RPC por stdio&lt;/strong&gt; contra la API REST de la plataforma. El agente lista, busca, comenta y &lt;strong&gt;redacta en borrador&lt;/strong&gt; desde la misma sesión en la que programa.&lt;/p&gt;
&lt;p&gt;Lo interesante no es el camino feliz, son los bordes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;La API no tiene campo &lt;code&gt;draft&lt;/code&gt;.&lt;/strong&gt; El borrador se implementa con &lt;code&gt;POST /posts&lt;/code&gt; seguido de &lt;code&gt;POST /posts/{id}/hide&lt;/code&gt;: el post nace invisible salvo para el autor y los mods. &lt;strong&gt;Publicar es &lt;code&gt;POST /posts/{id}/reshow&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El MCP nunca publica solo.&lt;/strong&gt; &lt;code&gt;cl_create_post&lt;/code&gt; valida y crea &lt;strong&gt;siempre oculto&lt;/strong&gt;; si el &lt;code&gt;hide&lt;/code&gt; falla, devuelve el &lt;code&gt;id&lt;/code&gt; y avisa de que el post quedó &lt;strong&gt;público&lt;/strong&gt; para corregirlo de inmediato. La publicación real solo ocurre con &lt;code&gt;cl_publish_post&lt;/code&gt;, y esa llamada la decido yo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edición incremental.&lt;/strong&gt; &lt;code&gt;cl_edit_post&lt;/code&gt; hace read-modify-write en servidor: &lt;code&gt;append&lt;/code&gt;, &lt;code&gt;prepend&lt;/code&gt;, &lt;code&gt;insert_before&lt;/code&gt;, &lt;code&gt;insert_after&lt;/code&gt; o &lt;code&gt;replace&lt;/code&gt; sobre un &lt;strong&gt;ancla literal&lt;/strong&gt;. Añado una sección sin reenviar 19 KB de markdown, con &lt;code&gt;dry_run&lt;/code&gt; para previsualizar antes de guardar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es el patrón que repito en todo: la API hace lo mínimo, el MCP añade la &lt;strong&gt;disciplina&lt;/strong&gt; (draft-first, revisión humana) y el agente se queda con lo mecánico. Si algo no lo puedo auditar o deshacer, no lo automatizo.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;de-la-tui-al-desktop-en-linux-ya-migrado-&quot;&gt;De la TUI al Desktop en Linux (ya migrado)&lt;/h3&gt;
&lt;p&gt;Hasta hace poco corría todo en la &lt;strong&gt;TUI&lt;/strong&gt; (v1.18.34). Acabo de dar el salto a la &lt;strong&gt;versión Desktop&lt;/strong&gt; en &lt;strong&gt;Linux Mint 22.3&lt;/strong&gt;, instalando el &lt;code&gt;.deb&lt;/code&gt; de la release oficial. En Linux no hay cask de Homebrew —eso es solo macOS—; lo correcto aquí son los paquetes de la release (&lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt; o &lt;code&gt;.AppImage&lt;/code&gt;). El binario de escritorio registra su lanzador en el menú de aplicaciones, así que paso de &quot;abrir una terminal y teclear&quot; a &quot;abrir una app&quot;. &lt;strong&gt;Misma configuración, misma memoria, mismo agente&lt;/strong&gt;; solo cambia el envoltorio. Si tienes dudas con la instalación en Linux, te lo cuento con detalle en los comentarios.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;diez-cosas-que-mi-agente-barato-hace-hoy&quot;&gt;Diez cosas que mi agente barato hace hoy&lt;/h3&gt;
&lt;p&gt;Ejemplos reales, no humo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Diagnosticar un repo&lt;/strong&gt;: señalarlo, que reproduzca, minimice y proponga un plan de fallos con la skill &lt;code&gt;diagnose&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escribir tests primero&lt;/strong&gt;: ciclo &lt;em&gt;red-green-refactor&lt;/em&gt; con &lt;code&gt;tdd&lt;/code&gt;, no tests de relleno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revisar un diff&lt;/strong&gt;: auditoría en paralelo de estándares y de especificación con &lt;code&gt;review&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditoría web real&lt;/strong&gt;: levantar el navegador headless y ejecutar un &lt;strong&gt;Lighthouse&lt;/strong&gt; sobre mi página.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consultar docs actualizadas&lt;/strong&gt; con Context7 y &lt;strong&gt;guardarlas en Engram&lt;/strong&gt; para no repetir la consulta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recordar un fix entre sesiones&lt;/strong&gt;: &quot;esto ya me falló&quot;, y el error→solución está ahí.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lanzar un build largo en background&lt;/strong&gt; y recibir una sola línea de estado (&lt;code&gt;done&lt;/code&gt;) en vez de 200 líneas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactorizar con plan&lt;/strong&gt;: &lt;code&gt;request-refactor-plan&lt;/code&gt; descompone en commits pequeños y seguros.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crear una skill propia&lt;/strong&gt; con &lt;code&gt;write-a-skill&lt;/code&gt; cuando repito un patrón.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delegar exploración&lt;/strong&gt; de un codebase desconocido a un subagente mientras sigo trabajando.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lo-honesto-qu-gano-y-qu-pierdo&quot;&gt;Lo honesto: qué gano y qué pierdo&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Gano&lt;/strong&gt;: coste de céntimos, memoria que persiste, permisos que respeto, cero telemetría y la tranquilidad de un sistema que puedo auditar y deshacer. &lt;strong&gt;Pierdo&lt;/strong&gt;: techo de razonamiento —un modelo Flash no es un modelo de frontera— y comodidad. Un agente barato &lt;strong&gt;no piensa por ti&lt;/strong&gt;; te obliga a decirle exactamente qué quieres y a verificar el resultado. Para mí, ese intercambio es una ventaja: es la diferencia entre &quot;confío porque lo leí&quot; y &quot;confío porque me lo dijo un modelo caro&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;takeaway-2&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;No necesitas el modelo más potente para producir software serio: necesitas &lt;strong&gt;economía de tokens, memoria y disciplina&lt;/strong&gt;. Con un modelo barato bien dirigido, el límite deja de ser el precio y pasa a ser tu criterio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Y tú? ¿Qué modelo barato usas —o usarías— y cómo compensas sus límites?&lt;/strong&gt; Cuéntamelo en los comentarios; leo y respondo a todo.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;&lt;strong&gt;English version below&lt;/strong&gt; · The original is in Spanish. If you're an English speaker, jump to the translation and let me know your setup in the discussion. I reply to everyone.&lt;/sub&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-cheap-setup-running-opencode-deepseek-for-cents-feat-igniciondev-&quot;&gt;My cheap Setup: running OpenCode + DeepSeek for cents (feat. IgnicionDev)&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;em&gt;My cheap Setup feat IgnicionDev&lt;/em&gt; — Post 00&lt;br&gt;
&lt;strong&gt;Read time:&lt;/strong&gt; ~8 min · &lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;ai&lt;/code&gt; &lt;code&gt;aiagents&lt;/code&gt; &lt;code&gt;devtools&lt;/code&gt; &lt;code&gt;mcp&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;i-built-an-agent-that-costs-cents-and-gave-me-control-back-not-noise&quot;&gt;I built an agent that costs cents and gave me control back, not noise&lt;/h3&gt;
&lt;p&gt;Most talk about coding agents starts with raw power: the biggest model, the highest benchmark, the flashiest demo. My problem was the opposite. I didn't need &lt;em&gt;more&lt;/em&gt; intelligence — I needed &lt;strong&gt;something that costs cents, doesn't flood my terminal, and produces output I can audit line by line&lt;/strong&gt;. If I can't read and run what the agent produces, it's useless to me: I work with money, invoices and data that has to add up.&lt;/p&gt;
&lt;p&gt;So I built a cheap agent —&lt;strong&gt;OpenCode + DeepSeek Flash&lt;/strong&gt;— and wrapped it in discipline: persistent memory, token-spend rules, strict permissions and real verification. This post is the &quot;how&quot;, with numbers, and it ends with &lt;strong&gt;ten concrete things&lt;/strong&gt; you can do today with a setup like this. No magic; just honest engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;who-s-writing-this-1&quot;&gt;Who's writing this&lt;/h3&gt;
&lt;p&gt;I'm Ignacio Badenes (&lt;strong&gt;IgnicionDev&lt;/strong&gt;), an IT technician from Castellón, Spain. I come from support and access control: protocols, logs, staying calm when things break. I now apply that habit to the software I build and publish —&lt;strong&gt;Aethernet&lt;/strong&gt; (local WiFi auditing, 146 tests), &lt;strong&gt;LocalGrant-Finder&lt;/strong&gt; (Spanish official-gazette grants with hybrid search), &lt;strong&gt;SystemBridge-AI&lt;/strong&gt; (invoices into a balanced ledger, money as &lt;code&gt;Decimal&lt;/code&gt;) and &lt;strong&gt;AssetShrink&lt;/strong&gt; (C++17, local image processing)—. All with tests, CI and a licence. If a tool isn't auditable, I don't ship it. Same goes for my agent.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-thesis-discipline-replaces-raw-power&quot;&gt;The thesis: discipline replaces raw power&lt;/h3&gt;
&lt;p&gt;A cheap model &lt;strong&gt;doesn't&lt;/strong&gt; force cheap results. It forces you to stop improvising. The trick isn't the model: it's &lt;strong&gt;spending few tokens without losing accuracy&lt;/strong&gt;. With DeepSeek Flash the cost splits like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cost = fresh_input × $0.14/M  +  cached_input × $0.0028/M  +  output × $0.28/M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cached input is &lt;strong&gt;50× cheaper&lt;/strong&gt; than fresh input, and output is the priciest line. So I attacked all three:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Minimal system prompt and tools&lt;/strong&gt; (fewer fixed tokens per request).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximum cache reuse&lt;/strong&gt; (same prefix, no random UUIDs to break it).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controlled output&lt;/strong&gt; (short answers or JSON, no filler paragraphs).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In my working scenario that cuts spend by ~&lt;strong&gt;82%&lt;/strong&gt;; once the cache is warm, marginal cost per request drops to a &lt;strong&gt;fraction of a cent&lt;/strong&gt;. It's not &quot;free&quot;, but it gets close. And it stays correct because &lt;strong&gt;I supply the discipline&lt;/strong&gt; where the model can't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-setup-opencode-deepseek&quot;&gt;The setup: OpenCode + DeepSeek&lt;/h3&gt;
&lt;p&gt;My model is &lt;code&gt;deepseek/deepseek-v4-flash&lt;/code&gt;, served through an OpenAI-compatible provider with caching enabled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &quot;model&quot;: &quot;deepseek/deepseek-v4-flash&quot;,
  &quot;provider&quot;: {
    &quot;deepseek&quot;: {
      &quot;npm&quot;: &quot;@ai-sdk/openai-compatible&quot;,
      &quot;options&quot;: { &quot;setCacheKey&quot;: true },
      &quot;models&quot;: {
        &quot;deepseek-v4-flash&quot;: {
          &quot;reasoning&quot;: true,
          &quot;interleaved&quot;: { &quot;field&quot;: &quot;reasoning_content&quot; }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three decisions that matter all day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automatic compaction&lt;/strong&gt; (&lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;prune&lt;/code&gt;, &lt;code&gt;reserved: 5000&lt;/code&gt;): the conversation prunes itself; I don't drag a huge history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool output limits&lt;/strong&gt; (&lt;code&gt;max_lines: 200&lt;/code&gt;, &lt;code&gt;max_bytes: 8192&lt;/code&gt;): an &lt;code&gt;npm install&lt;/code&gt; doesn't dump 200 lines in my face; if it truncates, I re-run it scoped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict permissions&lt;/strong&gt; (&lt;code&gt;webfetch: deny&lt;/code&gt;, read-only plan mode): the agent can't browse or write outside its lane without my approval.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Above all of this sits one file: my &lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt;. It isn't a wish list; it's a &lt;strong&gt;work router&lt;/strong&gt; with a protocol (understand → plan → small change → verify → review), an honest &lt;em&gt;Definition of Done&lt;/em&gt; (typecheck, tests, lint, reviewed diff) and spending rules. The model is cheap; the judgement isn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;memory-and-context-stop-repeating-what-it-already-knows&quot;&gt;Memory and context: stop repeating what it already knows&lt;/h3&gt;
&lt;p&gt;An agent without memory is an intern with amnesia: every session starts from zero and pays twice. I use the &lt;strong&gt;&lt;code&gt;engram + zerotoken&lt;/code&gt;&lt;/strong&gt; plugin: typed memory in SQLite with &lt;strong&gt;FTS5&lt;/strong&gt;, &lt;strong&gt;auto-saving&lt;/strong&gt; error→fix, architecture decisions and compactions, and injecting only &lt;strong&gt;2 memories / 600 tokens&lt;/strong&gt; at startup. In short: instead of dragging 50,000 tokens of history, I inject just enough.&lt;/p&gt;
&lt;p&gt;On top of that I add &lt;strong&gt;Context7&lt;/strong&gt;: when I ask about a specific API or version, it pulls the real documentation and &lt;strong&gt;caches it as a memory&lt;/strong&gt;. Next time I don't re-query it and the model doesn't hallucinate it. Live docs, zero repetition.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-opencode-conf-the-private-project-that-shapes-it-all&quot;&gt;My.OpenCode.Conf: the private project that shapes it all&lt;/h3&gt;
&lt;p&gt;Here's the part most people miss. My config isn't a loose &lt;code&gt;opencode.json&lt;/code&gt;: it's a &lt;strong&gt;private&lt;/strong&gt; GitHub project called &lt;strong&gt;My.OpenCode.Conf&lt;/strong&gt;, part of my learning and the way I work. It's a bundle of &lt;strong&gt;three independent subprojects&lt;/strong&gt; that talk to each other through an open protocol (&lt;strong&gt;MCP&lt;/strong&gt;):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Subproject &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;What it brings &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Built with&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;skill-matrix&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;240+ skills in &lt;code&gt;SKILL.md&lt;/code&gt; format (technical and design) the agent loads &lt;strong&gt;on demand&lt;/strong&gt; via triggers &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Markdown&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;second-termux-v2&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;An MCP server that runs &lt;strong&gt;background jobs&lt;/strong&gt; and returns &lt;strong&gt;a single status line&lt;/strong&gt; instead of flooding the TUI &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;TypeScript (ESM)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;engram+zerotoken&lt;/code&gt;&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;The &lt;strong&gt;persistent-memory&lt;/strong&gt; plugin (FTS5) and the token-saving policy &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;TypeScript (Bun)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A &lt;strong&gt;master prompt&lt;/strong&gt; ties them together: instead of maintaining a fragile &lt;code&gt;install.sh&lt;/code&gt;, &lt;em&gt;the agent configures itself&lt;/em&gt; (detects the OS, adapts commands). And, to me, the best part of its design: it's &lt;strong&gt;reversible&lt;/strong&gt;. It uninstalls cleanly, with no lock-in, no telemetry and without touching what you already had. The repo is private for now —I show it, explain it and answer questions— but the lesson is public: &lt;strong&gt;a good config is code you can read, verify and undo.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-tools-that-wire-it-together-mcp-and-plugins-&quot;&gt;The tools that wire it together (MCP and plugins)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;chrome-devtools&lt;/code&gt; (headless)&lt;/strong&gt; — a real browser the agent drives: navigate, extract, and run &lt;strong&gt;Lighthouse&lt;/strong&gt; audits without opening a window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;github&lt;/code&gt;&lt;/strong&gt; — issues, PRs and actions from the conversation itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sqlite&lt;/code&gt;&lt;/strong&gt; — queries against my local work database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;context7&lt;/code&gt;&lt;/strong&gt; — live library documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;human-followup.ts&lt;/code&gt;&lt;/strong&gt; — my own plugin using &lt;code&gt;notify-send&lt;/code&gt;: when the agent &lt;strong&gt;asks for input&lt;/strong&gt;, needs a &lt;strong&gt;permission&lt;/strong&gt;, or &lt;strong&gt;finishes&lt;/strong&gt; a navigation, I get a desktop notification. I can stop staring at the terminal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents&lt;/strong&gt; (&lt;code&gt;explore&lt;/code&gt;, &lt;code&gt;general&lt;/code&gt;) — to split exploration and heavy work without polluting my context.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;building-your-own-mcp-to-publish-without-leaving-the-agent&quot;&gt;Building your own MCP to publish without leaving the agent&lt;/h3&gt;
&lt;p&gt;The most honest example of a &quot;tool of my own&quot; is this very post. Instead of copy-pasting into a browser, I wrote a &lt;strong&gt;local MCP&lt;/strong&gt; for Coder Legion in &lt;strong&gt;dependency-free Node ESM&lt;/strong&gt; (~300 lines): a &lt;strong&gt;JSON-RPC conversation over stdio&lt;/strong&gt; against the platform's REST API. The agent lists, searches, comments and &lt;strong&gt;drafts&lt;/strong&gt; from the same session where it codes.&lt;/p&gt;
&lt;p&gt;What's interesting isn't the happy path, it's the edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The API has no &lt;code&gt;draft&lt;/code&gt; field.&lt;/strong&gt; The draft is implemented with &lt;code&gt;POST /posts&lt;/code&gt; followed by &lt;code&gt;POST /posts/{id}/hide&lt;/code&gt;: the post is born invisible to everyone but the author and mods. &lt;strong&gt;Publishing is &lt;code&gt;POST /posts/{id}/reshow&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The MCP never publishes on its own.&lt;/strong&gt; &lt;code&gt;cl_create_post&lt;/code&gt; validates and creates &lt;strong&gt;always hidden&lt;/strong&gt;; if the &lt;code&gt;hide&lt;/code&gt; fails it returns the &lt;code&gt;id&lt;/code&gt; and warns that the post went &lt;strong&gt;public&lt;/strong&gt; so I fix it immediately. The real publish only happens through &lt;code&gt;cl_publish_post&lt;/code&gt;, and that call is mine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental editing.&lt;/strong&gt; &lt;code&gt;cl_edit_post&lt;/code&gt; does a server-side read-modify-write: &lt;code&gt;append&lt;/code&gt;, &lt;code&gt;prepend&lt;/code&gt;, &lt;code&gt;insert_before&lt;/code&gt;, &lt;code&gt;insert_after&lt;/code&gt; or &lt;code&gt;replace&lt;/code&gt; against a &lt;strong&gt;literal anchor&lt;/strong&gt;. I add a section without resending 19 KB of markdown, with &lt;code&gt;dry_run&lt;/code&gt; to preview before saving.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's the pattern I keep repeating: the API does the bare minimum, the MCP adds the &lt;strong&gt;discipline&lt;/strong&gt; (draft-first, human review) and the agent keeps the mechanical part. If I can't audit or undo it, I don't automate it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-tui-to-desktop-on-linux-already-migrated-&quot;&gt;From TUI to Desktop on Linux (already migrated)&lt;/h3&gt;
&lt;p&gt;Until recently I ran everything in the &lt;strong&gt;TUI&lt;/strong&gt; (v1.18.34). I've just moved to the &lt;strong&gt;Desktop&lt;/strong&gt; version on &lt;strong&gt;Linux Mint 22.3&lt;/strong&gt;, installing the official release &lt;code&gt;.deb&lt;/code&gt;. There's no Homebrew cask on Linux —that's macOS only—; here the right choice is the release packages (&lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt; or &lt;code&gt;.AppImage&lt;/code&gt;). The desktop binary registers its launcher in the application menu, so I go from &quot;open a terminal and type&quot; to &quot;open an app&quot;. &lt;strong&gt;Same config, same memory, same agent&lt;/strong&gt;; only the wrapper changes. If you're stuck installing it on Linux, I'll walk you through it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ten-things-my-cheap-agent-does-today&quot;&gt;Ten things my cheap agent does today&lt;/h3&gt;
&lt;p&gt;Real examples, no hype:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Diagnose a repo&lt;/strong&gt;: point at it, and it reproduces, minimises and proposes a fix plan using the &lt;code&gt;diagnose&lt;/code&gt; skill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write tests first&lt;/strong&gt;: &lt;em&gt;red-green-refactor&lt;/em&gt; with &lt;code&gt;tdd&lt;/code&gt;, not filler tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review a diff&lt;/strong&gt;: a parallel audit of standards and spec with &lt;code&gt;review&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real web audit&lt;/strong&gt;: spin up the headless browser and run &lt;strong&gt;Lighthouse&lt;/strong&gt; on my site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pull up-to-date docs&lt;/strong&gt; with Context7 and &lt;strong&gt;store them in Engram&lt;/strong&gt; so the query isn't repeated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remember a fix across sessions&lt;/strong&gt;: &quot;this broke before&quot;, and the error→fix is there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run a long build in the background&lt;/strong&gt; and get a single &lt;code&gt;done&lt;/code&gt; status line instead of 200.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor with a plan&lt;/strong&gt;: &lt;code&gt;request-refactor-plan&lt;/code&gt; breaks it into small, safe commits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create its own skill&lt;/strong&gt; with &lt;code&gt;write-a-skill&lt;/code&gt; when I repeat a pattern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delegate exploration&lt;/strong&gt; of an unknown codebase to a subagent while I keep working.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-honest-part-what-i-gain-and-what-i-lose&quot;&gt;The honest part: what I gain and what I lose&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;I gain&lt;/strong&gt;: cents-level cost, memory that persists, permissions I respect, zero telemetry, and the peace of mind of a system I can audit and undo. &lt;strong&gt;I lose&lt;/strong&gt;: a reasoning ceiling —a Flash model isn't a frontier model— and convenience. A cheap agent &lt;strong&gt;doesn't think for you&lt;/strong&gt;; it forces you to say exactly what you want and to verify the result. To me, that trade is an advantage: it's the difference between &quot;I trust it because I read it&quot; and &quot;I trust it because an expensive model said so&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;takeaway-3&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;You don't need the most powerful model to produce serious software: you need &lt;strong&gt;token economy, memory and discipline&lt;/strong&gt;. With a cheap model well directed, the limit stops being the price and becomes your judgement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So what about you? Which cheap model do you use —or would you use— and how do you compensate for its limits?&lt;/strong&gt; Tell me in the comments; I read and reply to everyone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;sub&gt;Licencia de los proyectos mencionados: MIT · Sin secretos ni claves en este post. · Proyectos: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/yosoyignicion&quot;&gt;github.com/yosoyignicion&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/29882/mi-setup-de-agente-barato-opencode-deepseek-por-centimos-feat-igniciondev</guid>
<pubDate>Mon, 05 Oct 2026 15:40:22 +0000</pubDate>
</item>
</channel>
</rss>