<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>gitgood.sh — from the trenches</title>
    <link>https://gitgood.sh/from-the-trenches</link>
    <description>Field notes from production. Opinions, earned the hard way.</description>
    <language>en-gb</language>
    <copyright>© 2026 Lukasz Wisniewski</copyright>
    <lastBuildDate>Tue, 09 Jun 2026 22:50:04 +0000</lastBuildDate>
    <atom:link href="https://gitgood.sh/rss" rel="self" type="application/rss+xml"/>
    <generator>gitgood.sh v0.3.0</generator>
    <image>
      <url>https://gitgood.sh/favicon.png</url>
      <title>gitgood.sh — from the trenches</title>
      <link>https://gitgood.sh/from-the-trenches</link>
      <width>128</width>
      <height>128</height>
    </image>
    <item>
      <title>Driver, not passenger</title>
      <link>https://gitgood.sh/from-the-trenches/driver-not-passenger</link>
      <guid isPermaLink="true">https://gitgood.sh/from-the-trenches/driver-not-passenger</guid>
      <pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
      <dc:creator>Lukasz Wisniewski</dc:creator>
      <description>How I use LLMs in daily engineering — and where I draw the line</description>
      <content:encoded><![CDATA[<p>The label AI has always tracked hype, not capability. The research field has been an active discipline since the <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Dartmouth_workshop">Dartmouth workshop</a> in 1956 — McCarthy, Minsky, Shannon. The target was general intelligence — and hasn't moved.</p>
<p>The label is industry shorthand. Nobody who builds game AI thinks they're building intelligence. They're shipping a system that looks alive for the few minutes the player is paying attention to it. The same applies to LLMs.</p>
<h2>LLMs are statistical autocomplete</h2>
<p>LLMs are built on top of neural nets. The job is simple: given a chunk of text, guess the next token. Train one long enough — on most of the internet, on every book in the library, on a few decades of code — and the guesses start to look smart. None of it is intelligence — it's a very thorough study of what token usually follows what.</p>
<p>The model doesn't know. It predicts. No experience, no emotion, no knowledge, no critical thinking, no wisdom — none of the human factors that produce understanding. There's a model of what comes next, one token at a time.</p>
<blockquote>
<p>What an LLM can produce, once harnessed, is absolutely remarkable.</p>
</blockquote>
<h2>Vibes all the way down — what a freaking nonsense</h2>
<p>The term <em>vibe coding</em> makes me puke 🤮 <em>&quot;What you vibing?&quot;</em> — one may ask. Just button it... <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Andrej_Karpathy">Andrej Karpathy</a> came up with the term in early 2025. Everyone has been parroting it since, without thinking.</p>
<p>I don't want to have anything to do with vibe coders. Vibe coding is letting an LLM run loose, building <code>black boxes</code> with the attitude of <em>&quot;ahh whatever, if it breaks I ask <code>clanker</code> to fix it.&quot;</em> That's asking for trouble. It may build you a simple thing — but a demo has nothing to do with a production-ready system.</p>
<p>The outcome is a tangled mess. Spaghetti code looks organised next to it. No structure. No architecture. Everything dumped in one file. Unnecessary code everywhere. Security holes bigger than the ones in Swiss cheese. Performance issues. Just a random mess assembled from random snippets that almost works.</p>
<h2>AI assisted development</h2>
<p>This is the term I like to use. I've been an incredibly heavy LLM user since day one — I hate it as much as I love it. My favourite joke about all this: if AI ever gets sentient, I'm at the top of the hit list. The amount of abuse I offload onto the models to find the right workflow has made sure of it.</p>
<p>There's no golden bullet. Everyone is different. Everyone works differently. Everyone has to find their own way to make it work. It works for me — and believe me, I put that thing through its paces daily.</p>
<p>One thing is undeniable: LLMs do better in some areas than others. Code generation tracks the training data — the more there is for a language, the better the output. For popular ones, decent. For niche ones, worse. For <code>Odin</code>, the models absolutely suck 🤷</p>
<p>When pointing LLM at the codebase, the quality of that codebase matters a lot. Garbage in, garbage out. The model imitates what it reads. The same applies when generating from nothing — the training data quality is questionable, the output mediocre at best. This is where moderation comes in.</p>
<h2>How I incorporate LLMs into my daily engineering</h2>
<p>AI is just another automation tool. Nothing else. All it does is save me time — sometimes a lot of it. Most of programming work is boring anyway: simple, repetitive code. In the grand scheme of things, the internet is basically CRUD. Generating code speeds my workflow — I don't have to type it.</p>
<p>The difference is I'm in full control. I own the architecture. I decide the code organisation. I follow every line. I push back when needed. I ask for changes. I give directions. I ask for specific implementations. I verify what the model claims. I refuse output I don't understand. I iterate. I adjust manually. I refactor what doesn't fit. Small iterations work. <em>&quot;Build me a Facebook&quot;</em> doesn't 😂 The model is the tool. I'm still the engineer. If I'm not going to end up with code I'd write by hand, it has no place to exist.</p>
<blockquote>
<p>The code that ships under my name is mine — accelerated.</p>
</blockquote>
<p>I use the LLM to research things I don't know — cross-referencing sources, parsing specs, drafting documentation. Faster than the manual route.</p>
<p>LLMs are invaluable when I'm learning new concepts and new languages. The rate at which I can pick up new ground is intoxicating — weeks of reading compressed into hours. Moderation, again. Never trust an LLM. Never. If it tells me something I don't know, I cross-reference. They hallucinate — that's how they work.</p>
<p>And of course, writing — which LLMs excel at. But there's a big but...</p>
<h2>Ghost in the prose</h2>
<p>I'm sure it doesn't surprise you: this very article was written with an LLM's help. The distinction matters — <em>with</em> help, not generated. There is no point hiding it. I'm allergic to slop. The kind of AI writing that <em>delves into</em> every paragraph, opens every post with <em>in an era of</em>. I won't ship it.</p>
<p>Yeah, I could prompt it. Give it a title. Ask it to fill in some generic SEO-friendly text. No voice. No personality. Nothing interesting. Just slop. The internet is already drowning in it. I won't be adding to it.</p>
<p>Working on a <code>RAG</code> system recently, I discovered something fun: AI detectors are worthless. I fed one the full text of <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/The_Last_Wish">The Last Wish</a> by Andrzej Sapkowski — published in 1993. The verdict: AI-generated, 60% confidence. (Yes, kids — <em>The Witcher</em> is from the 80s.)</p>
<p>Here's how I write. Hours of thinking. Collecting notes and anecdotes. Research. A draft written by hand. Then the LLM comes in — proofreading, grammar, awkward phrasing. More reworking. Shuffling lines around. Reading the thing a hundred times to make sure the words flow, the rhythm holds, the whole thing is coherent. Writing is a long, hard process of constant refactoring. The same applies to code. Neither will ever be perfect — it just has to be good enough. Make your own judgment based on that.</p>
<h2>The model isn't in <code>git blame</code></h2>
<p>Unless it's vibe coding, then it is. There is absolutely nothing wrong with AI-generated code. None of it bothers me. What bothers me is companies bragging about the percentage of AI-generated code in their codebase. <em>80% of our code is AI-generated.</em> It doesn't matter. It's all hype-driven. It's not an achievement. Code is a means to an end — shipping a product. The end user doesn't care if the app was written in <code>Rust</code>, <code>Ruby</code>, or <code>PHP</code>. They don't care if it was written by AI. They want to get their stuff done. They want to use the product. If they don't, you're in trouble — but the code isn't the problem, if it's good enough. The product is.</p>
<p>Shipping a reliable product is a choice. A decision. Engineering is a craft — care, knowledge, judgment, experience. How you get there doesn't matter. A Sunday driver and an F1 driver both have a car. The car needs a driver.</p>
<blockquote>
<p>What matters is the end result: software that's reliable, performant and secure.</p>
</blockquote>
]]></content:encoded>
      <category>ai</category>
      <category>llms</category>
      <category>ai-assisted-development</category>
      <category>engineering</category>
    </item>
    <item>
      <title>TDD is a costume</title>
      <link>https://gitgood.sh/from-the-trenches/tdd-is-a-costume</link>
      <guid isPermaLink="true">https://gitgood.sh/from-the-trenches/tdd-is-a-costume</guid>
      <pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate>
      <dc:creator>Lukasz Wisniewski</dc:creator>
      <description>Tests do the work - TDD adds the ritual</description>
      <content:encoded><![CDATA[<p>If you write tests close to the code, your tests start shaping it. They expose tangled dependencies. They signal when a function is doing too much. They make you think about the interface before you commit to internals. This is the design feedback TDD takes credit for. You get most of it from any test written close to the code — not just from tests written first.</p>
<p>There's a broader version of the argument: writing tests first enforces design upfront. It doesn't. Design happens in your head before you write a line of anything. Tests are one way to commit a design, not a prerequisite for having one.</p>
<p>There are many ways to test software — manual, end-to-end, contract, integration, property-based, load, exploratory. TDD evangelism lives in exactly one of them: unit tests. The rest of the surface area is somebody else's problem.</p>
<p>TDD looks great on toy examples. <em>Hey, let's TDD a stack.</em> Push, pop, peek — three methods, no I/O, no dependencies, no surrounding system. That's not reality. Reality is a flaky third-party API, a payment gateway that times out, a schema that changed last week...</p>
<h2>What actually happens</h2>
<p>Someone announces they &quot;do TDD.&quot; Pause. Repeat the announcement to anyone who walks past their desk. Then describe — at length — the colors of their feedback loop. Red. Green. Refactor. Like the alphabet, in case you forgot.</p>
<h3>Bob</h3>
<p>Not that long ago, I had an interview with a TDD evangelist. Let's call him Bob — to spare him the exposure. Bob wears a TDD hat the way some people wear tin foil — proud, tight, slightly off. Bob told me he writes tests first because — and I'm quoting — &quot;I know what part of the system was tested.&quot; Two things went through my head:</p>
<ul>
<li>How does someone like this actually get employed?</li>
<li>I'm talking to a badly trained model.</li>
</ul>
<p>Frankly, I still don't fully understand what Bob meant by it. I suppose it was his winning argument. I disengaged with &quot;I've never had your problems, so I don't know what you're going on about, mate.&quot; Not the most diplomatic interview move. An interview is a two-way process though — I'd rather wait for the right opportunity.</p>
<p>The only honest record of what's been tested is the code. The opinion collapses on first contact with a team. Code is shipped by groups of people. If &quot;what was tested&quot; only lives in the one engineer's head, the reviewer, the on-call, the new hire six months from now — they all get nothing. That's not engineering discipline. It's personal note-keeping wearing a methodology hat.</p>
<h3>The screenshot trophy</h3>
<p>Then there's the social-media variant: a screenshot of green checkmarks running down a test list, posted to social media or a Slack channel for everyone to see. Some people share photos of their food. But posting a passing test as a milestone is the tell of someone new — trying to impress everyone with the bare minimum.</p>
<blockquote>
<p>Testing is standard engineering practice. It's part of the job, not a milestone.</p>
</blockquote>
<h2>Tests are not magic</h2>
<p>Every test is more code. And every line of code is a maintenance liability. The more code in the repository — written, copy-pasted, AI-generated — the higher the probability of bugs. Tests don't escape that math. They're code too.</p>
<p>Tests reduce bugs. They aren't a magic safety net. A test existing doesn't mean the code under it is bug-free — it just means it was checked. And every test you write is another surface to maintain, another file that has to be updated when the underlying behaviour changes, another thing that can be wrong. A bad test will lie about what the code does. A flaky test will train the team to ignore failures. A test that exists only to bump coverage is dead weight in the water.</p>
<p>A test suite is only as good as the engineer who wrote it. If the engineer doesn't understand what should be true about the system, the tests will codify the misunderstanding — and now the misunderstanding has a green checkmark next to it.</p>
<h2>Not all code earns a test</h2>
<p>Plenty of code doesn't need a test:</p>
<ul>
<li>Language features</li>
<li>Standard library functions</li>
<li>The framework's own well-tested behaviour</li>
</ul>
<p>Writing tests for <code>1 + 1</code> because the linter says branch coverage dropped is busywork with a green checkmark on it.</p>
<p>Behavioural testing is what matters — what the function does for the system, not what each line evaluates to. The arithmetic is the language's job. The edge cases aren't — floating-point rounding, integer overflow, the boundaries where the runtime starts doing unexpected things. Those earn their tests.</p>
<blockquote>
<p>A unit isn't a function — it's a unit of behaviour that can be one function or a whole module. The size doesn't matter. The boundary does.</p>
</blockquote>
<p>What gets tested is judgment — the thing in production with stakes, the business rule that has to hold, the boundary condition the language won't catch. Coverage requirements treat every test as equal. That's the bug.</p>
<h2>Monkey tests</h2>
<p>The moment coverage becomes a CI gate, behaviour changes. The team stops asking &quot;does this work?&quot; and starts asking &quot;does this branch get visited?&quot; The result: code that walks the runtime through every line, asserts nothing meaningful and ticks the coverage percentage up.</p>
<p>Coverage doesn't tell you what was tested. It tells you what was hit. A 95% coverage number means the runtime walked over 95% of the lines. It says nothing about whether any of those lines did the right thing under any of the inputs they'll actually see.</p>
<p>A strict coverage threshold trains the team to write monkey tests. The number goes up, the bug count stays the same and the suite gets heavier every sprint with code that asserts nothing.</p>
<h2>Where tests pay off</h2>
<p>People like to say a well-tested codebase gives you fearless refactoring. True — to a degree. Rename a function, restructure a module, swap an internal implementation — and the suite tells you what broke. It's a double-edged sword though — passing tests don't mean nothing broke, only that the tests still pass. Test counts, coverage numbers, methodology stickers on a laptop — none of that ships better software.</p>
<p>The size of the test suite is influenced by the language choice. Statically-typed languages catch the dumb mistakes at compile time — wrong type, missing field, function signature mismatch. The type checker is built in, not bolted on. Loosely-typed languages don't have that backstop, so they bolt one on — <code>TypeScript</code> on top of <code>JavaScript</code>, <code>mypy</code> on top of <code>Python</code> Different language, different overhead. What gets tested is what the tooling won't catch.</p>
<blockquote>
<p><strong>The ceremony doesn't matter.</strong> <em>Balancing a pogo stick on a beach ball</em> doesn't matter. What matters is the end result: software that's reliable, performant and secure.</p>
</blockquote>
<p>Exorcisms like this are the engineering version of insisting tea must be stirred clockwise — that stirring it anti-clockwise spoils it. The tea doesn't care. The ritual is for the person performing it — they might as well sacrifice a chicken on the last day of the month while they're at it.</p>
]]></content:encoded>
      <category>tdd</category>
      <category>testing</category>
      <category>engineering-culture</category>
    </item>
  </channel>
</rss>
