<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[WessCoby]]></title><description><![CDATA[Exploring the craft of building for the web—from React & TypeScript on the frontend to Node.js, Express, Koa, and NestJS on the backend. I also write about DevO]]></description><link>https://wesscoby.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1600255527504/_Bxea8Kkk.png</url><title>WessCoby</title><link>https://wesscoby.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 15:11:17 GMT</lastBuildDate><atom:link href="https://wesscoby.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Reason I Could Say Out Loud]]></title><description><![CDATA[loadout's bash backend was the last phase. Phase 7, shipped after credential switching, after a web interface, after git-backed config sync, after macOS support — on a project that exists because bash]]></description><link>https://wesscoby.hashnode.dev/a-reason-i-could-say-out-loud</link><guid isPermaLink="true">https://wesscoby.hashnode.dev/a-reason-i-could-say-out-loud</guid><category><![CDATA[Go Language]]></category><category><![CDATA[command line]]></category><category><![CDATA[developer experience]]></category><category><![CDATA[dotfiles]]></category><dc:creator><![CDATA[WessCoby]]></dc:creator><pubDate>Sat, 12 Sep 2026 10:55:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5d45f34dc88cb84c1499c389/3edee9ba-5633-4542-a026-e5070c33c678.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>loadout's bash backend was the last phase. Phase 7, shipped after credential switching, after a web interface, after git-backed config sync, after macOS support — on a project that exists because bash did not work.</p>
<p>The reason it exists is real, and I can point at it. I tried to use my aliases on an Ubuntu server and could not: the server ran bash, my functions were zsh, and zsh functions in bash do not fail. They run and quietly do nothing. That is a genuine problem, I have the commits that fix it, and it is not why I started.</p>
<p>The bash mismatch was the first thing I noticed and the least interesting. The box also had aliases for tools that were not on the box — commands standing ready to fail, in my own handwriting. And it had the binaries. The dotfiles repo carried 61MB of committed third-party executables, duckdb and ngrok among them, and they arrived on that server because they arrived everywhere. I did not need duckdb there. I might need it on the next machine. The repo had no way to hold both of those facts at once, because nobody had ever told it there was more than one machine.</p>
<p>That is not a configuration. That is luggage.</p>
<p>What was wrong was not any of those three things on its own. It was that cloning a dotfiles repo <strong>replays</strong> a machine. It re-enacts the history of the laptop it grew on — that zsh is there, that duckdb is wanted, that ngrok is on PATH — onto a machine with a different history. Every assumption travels, because the repo is a record of where I had been rather than a description of what I wanted.</p>
<p>So I wrote something that <strong>renders</strong>. One catalog of aliases and functions, one configuration file holding the personal parts — who I am in git, where my projects live — and a command that writes a single file the shell sources. The binaries stopped travelling: they are declarations now, fetched on the machine that wants them and checksummed before they go on PATH. Two shells, two platforms: the same catalog comes out four different ways, and the repo keeps all four on file.</p>
<p>A replay has one output. A render has as many as there are machines.</p>
<p>The server was not the first time. It was the last one. I had installed that setup on other machines before it, and the wanting had been building the whole way. The server is only where I stopped absorbing it.</p>
<p>Which brings back the phase order. The bash backend came last for a defensible reason: once the generator had a backend interface, a second dialect stopped being the hard problem, so it waited because it could afford to wait. That is true, and I would say it in a code review.</p>
<p>It is also not the reason. Bash was the excuse. I already wanted to build this, and the server handed me something I could say out loud.</p>
<p>There is a second one. Before I started I looked at chezmoi, stow, dotbot and the other tools that already exist, and concluded that none of them solved everything I wanted solved. I believe that too. And I really just wanted to build my own. I am not sure the research would have changed my mind if it had come out the other way.</p>
<p>The third is the web interface. <code>loadout ui</code> serves a page on localhost for composing the configuration, and it exists because I wanted to build a web interface. That one has no sayable version. I did not go looking for one.</p>
<p>It is also the most expensive thing in the project. A page that edits what your shell runs at login is a remote code execution API for your own account, so three checks precede every request: a per-run token, an Origin that has to be ours, and a Host that has to be loopback. The last of those is the defence against DNS rebinding, and there is a note in the repo telling me not to loosen it.</p>
<p>It broke in the stupidest way available. The interface served no CSS and no JavaScript at all, because the guard demanded a token on every request and a <code>&lt;script&gt;</code> tag cannot present one. A blank page — produced by the security I added to protect the feature I wanted for no reason.</p>
<p>None of that makes the tool fake. The problems were real, and looking at them now they are one problem wearing three coats: each of them failed without saying so.</p>
<p>The zsh functions on that Ubuntu box did not error. They ran and did nothing. The aliases for tools that were not installed did not announce themselves either — they waited until I typed one. And <code>loadout env</code>, the command that prints export lines for the active credentials, wrote them to stderr, so <code>eval "$(loadout env)"</code> — the exact line in my own README — did nothing at all. Nothing had ever run it end to end. Everything that had looked at it had looked at the text, and the text was right.</p>
<p>So the parts of loadout I am least able to claim I built for fun are the parts that make silence audible. <code>loadout doctor</code> walks everything the catalog asks for and reports what this machine does not have. <code>loadout describe</code> says what a command needs before you call it.</p>
<p>And <code>doctor</code> only reports. The alias for the missing tool still gets written into your file: the render varies by shell and by platform, not by what happens to be installed. So the second of my three failures is the one loadout does not fix — it makes it audible, which is not the same thing. That is at least consistent. loadout does not edit your shell config for you either; the line that sources the generated file is yours to add.</p>
<p>The four outputs from earlier have a name. They are golden files: the complete generated shell for both dialects on both platforms, pinned in the repository. Any change to a template or the catalog shows up as a diff across them, and that diff is the review — because the alternative is that a one-line edit to an alias becomes a silent change to every shell that runs <code>apply</code> next.</p>
<p>One more cost, and it is the one I pay rather than the one I shipped. Secrets in loadout live in the OS keychain and never in the generated file, because a generated file is plain text on disk. Something therefore has to fetch them at runtime — and the line that does it, if you put it in your shell config, reads the keychain every time you open a terminal and puts your tokens into the environment of every process you launch from it. The README lays both sides out and refuses to choose between them.</p>
<p>I chose. The line is in my <code>.zshrc</code>. I wrote that cost down before I took it.</p>
<p>It is on that server now. In bash. Working. My configuration lives in a git repository and moves between the machines, which was the founding problem and is not one any more.</p>
<p>One person who is not me has installed loadout. A colleague. No bugs reported, and nothing in the tool changed when they installed it, because I had already built it so that someone besides me could use it. There is a <code>CODE_OF_CONDUCT.md</code> in that repository, and a <code>CONTRIBUTING.md</code>, and a Homebrew tap in a repo of its own, and an install script served from a domain I own. The user base is one colleague. I built for an audience that has not arrived.</p>
<p>loadout is the first of several. makery.tools is where the rest of them go.</p>
<p>So "I wanted to build my own" is not something that happened to this project. It is the practice. The excuse changes every time. The sentence underneath it does not.</p>
]]></content:encoded></item><item><title><![CDATA[The Second Test Suite]]></title><description><![CDATA[This repository has no test framework. It has never had one, and it has still caught more of my mistakes than any project I have written tests for.
I have written more consequential English in this pr]]></description><link>https://wesscoby.hashnode.dev/the-second-test-suite</link><guid isPermaLink="true">https://wesscoby.hashnode.dev/the-second-test-suite</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[AI]]></category><category><![CDATA[developer experience]]></category><dc:creator><![CDATA[WessCoby]]></dc:creator><pubDate>Thu, 10 Sep 2026 07:52:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5d45f34dc88cb84c1499c389/c01049c1-8b69-449c-b11c-628df6a94809.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This repository has no test framework. It has never had one, and it has still caught more of my mistakes than any project I have written tests for.</p>
<p>I have written more consequential English in this project than TypeScript. Not documentation — nobody reads it as documentation. A file of rules, in prose, that gets enforced on work I am not watching, at a level of detail I could not sustain by reviewing. It is the highest-leverage artefact in the repository and it is not code.</p>
<p>A seed script in this project was about to grow a line reading <code>body: project.body ?? null</code>. It looks like a default. What it does, on every run of <code>yarn db:seed</code>, is overwrite every hand-written project writeup with nothing.</p>
<p>It never ran. What stopped it was not a test, because there are none. It was not review, because I never saw the line — it existed for about a minute, between being written and being rewritten. It was a sentence in the rules file, ten days old, a rule I had set and the agent had written down while building the thing the rule describes:</p>
<blockquote>
<p>It does not delete, so it will not eat rows created through the dashboard.</p>
</blockquote>
<p>The agent read its own note back, checked the line it was about to add against it, and stopped.</p>
<p>That sentence is not unusual. It is one line of the second of three layers, and the three are worth separating because they fail differently.</p>
<ol>
<li><p><code>yarn verify</code> — three suites, about a hundred and seventy checks, covering everything a machine can assert. A forged session cookie is refused. No form is nested inside another. Two rows never share a position. A reorder does not move <code>updatedAt</code>.</p>
</li>
<li><p><code>CLAUDE.md</code> — everything a machine cannot. <em>A stale NOW block is worse than none. Position is not content. Mono means data. No invented statistics. A missing value is an em dash, not a plausible sentence.</em></p>
</li>
<li><p><strong>My eye.</strong> The last resort.</p>
</li>
</ol>
<p>Layer one cannot express taste. Layer three does not survive a long afternoon. Layer two is prose, and it is the only one of the three that scales to work I am not watching.</p>
<p>Layer three earns its place, and the way it earns it is stupid.</p>
<p>Eleven words, typed while looking at a screen: "on project edit page, selecting categories does not have any indicator." That was my entire contribution. What it turned out to be was every filter chip in the application carrying a selected state that did nothing at all — four of them, in four files: the project editor, two admin lists, and the public work grid.</p>
<p>The cause is a cascade rule almost nobody has needed to know. The design system's classes are plain, unlayered CSS. Tailwind's utilities live in <code>@layer utilities</code>. Unlayered CSS beats layered CSS no matter how specific the layered rule is — so <code>bg-foreground</code> on a selected chip lost, silently, to the class's own <code>background: none</code>.</p>
<p>A selected chip and an unselected one computed to the same value: <code>rgba(0, 0, 0, 0)</code>, both. It had shipped, and it had passed review, because there is nothing in a diff to see. The code says <code>bg-foreground</code> and it means it.</p>
<p>I found it by saying a sentence a five-year-old could have said. The agent found the cause in about a minute once it was pointed at it. Neither half of that was optional.</p>
<p>Except that layer three has a shape, and the shape is not flattering.</p>
<p>Everything I caught was visible. A chip with no selected state. A page that had drifted from the design. A whole case-detail page that was simply missing. A cover restating the title of the page it sat on. All of it found the same way: by looking at a rendered screen and saying a plain sentence about it.</p>
<p>Everything I missed was invisible mechanics. The seed line is the example that got close enough to matter — a default that reads as a default and behaves as a delete.</p>
<p>That is not a lapse I can fix by concentrating harder. It is a property of eyes. A rendered page is the only artefact I review at full attention, and a whole class of mistake never appears on one.</p>
<p>Which is what the second test suite actually is. Not insurance, not documentation, not a style guide — the layer that covers precisely what a person looking at a screen cannot see.</p>
<p>The version of this era I keep reading has the engineer prompting and merging. That is not the trade.</p>
<p>The typing did shrink — genuinely, and more than I expected. What replaced it was not free time. It was checking: the logic, the interactions, the edge cases, whether the thing that was built is the thing that was meant. More time, not less.</p>
<p>The other half of the trade is stranger and nobody mentions it. Planning got <em>cheaper</em>. Describing what I actually want, in detail, before anything exists, used to be a tax paid against a slow build — you could think for a day and still wait a week. Now the build is the fast part, so thinking in detail up front is the cheapest move available.</p>
<p>So the work did not shrink. It moved to the two ends and left the middle: intent at the front, verification at the back, and a rapidly filled gap between them that I no longer type by hand.</p>
<p>Which leaves the question the title was avoiding.</p>
<p>My own instinct was that this makes me a product owner: thinking in product rather than in code. Right instinct, wrong word. A product owner sees the visible, and I was good at that here — but a product owner would not know that <code>?? null</code> in a seed script eats content, or that a reorder must not touch a timestamp the site derives its freshness from, or that an unlayered class beats a layered one. None of those are product judgements. They are the reasons layer two has anything in it, and only someone who can read the code knows to write them down.</p>
<p>So: product thinking chooses what to build. The engineering is knowing what will quietly go wrong, and saying so in advance, in English, in a file something else will read.</p>
<p>Every rule in layer two started as something that went wrong once. Which means the file is not a style guide. It is a list of things I have already paid for.</p>
]]></content:encoded></item></channel></rss>