<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <id>https://blog.davep.org</id>
  <title>davep</title>
  <updated>2026-05-20T18:37:01.919136+00:00</updated>
  <link href="https://blog.davep.org/feeds/ai.atom.xml" rel="self"/>
  <link href="https://blog.davep.org" rel="alternate"/>
  <generator uri="https://lkiesow.github.io/python-feedgen" version="1.0.0">python-feedgen</generator>
  <subtitle>Posts in category "AI" from davep</subtitle>
  <entry>
    <id>https://blog.davep.org/2026/05/20/the-gemini-bait-and-switch.html</id>
    <title>The Gemini bait and switch</title>
    <updated>2026-05-20T17:04:26+01:00</updated>
    <content type="html">&lt;p&gt;Well, what a surprise, nobody could have seen it coming: it does seem to be
&lt;a href="https://blog.davep.org/2026/05/13/the-copilot-bait-and-switch.html"&gt;bait and switch season&lt;/a&gt; in
LLM/agent land.&lt;/p&gt;
&lt;p&gt;As I mentioned &lt;a href="https://blog.davep.org/2026/05/20/goodbye-gemini-cli.html"&gt;earlier today&lt;/a&gt;, when I
ran up &lt;a href="https://blog.davep.org/tag/gemini/"&gt;Gemini CLI&lt;/a&gt; to have it work on a change to
&lt;a href="https://blog.davep.org/tag/blogmore/"&gt;BlogMore&lt;/a&gt; in the background, I got a notification that I
should be swapping to &lt;a href="https://blog.davep.org/tag/antigravity/"&gt;Antigravity CLI&lt;/a&gt; instead. I let
Gemini CLI get on with the change anyway, but resolved to install
Antigravity CLI and give it a go. While there's still a touch under a month
of use of Gemini CLI to go (based on the blog post), it seems sensible to
get to know the new tool as soon as possible.&lt;/p&gt;
&lt;p&gt;Installing Antigravity was a little bit of a faff. Looking at the
documentation, you have to install the main application itself first,
authorise with that, and then you can install and use the CLI. Fair enough.
Rather than download the DMG from their website, I decided to go with the
Homebrew installation (I like to try and keep track of what I have installed
and this helps me do that).&lt;/p&gt;
&lt;p&gt;So I installed that, ran it up, went through some setup questions, then
finally got dropped in something that looked like it wanted to be an IDE of
sorts. Nah, I'm fine, &lt;a href="https://blog.davep.org/tag/emacs/"&gt;I like to work elsewhere&lt;/a&gt;. But that was
okay given that I just wanted to get to &lt;a href="https://antigravity.google/product/antigravity-cli" rel="noopener noreferrer" target="_blank"&gt;the
CLI&lt;/a&gt; anyway. Before I
did that though, having installed this app, I saw that &lt;em&gt;it&lt;/em&gt; was showing a
&lt;em&gt;"Restart to update"&lt;/em&gt; notification. So I did that, waited a wee while, and
then finally was presented with something that looked totally different. Now
I had an application that looked almost exactly like the main Gemini website
(or &lt;a href="https://gemini.google/mac/" rel="noopener noreferrer" target="_blank"&gt;the Gemini macOS application&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;So that was kind of weird.&lt;/p&gt;
&lt;p&gt;Finally I was in a position to install the CLI itself. From what I can see
it's not available via Homebrew yet, and the &lt;a href="https://antigravity.google/download#antigravity-cli" rel="noopener noreferrer" target="_blank"&gt;installation
instructions&lt;/a&gt; are the
usual &lt;em&gt;"curl this through bash, trust me bro"&lt;/em&gt; affair. Having done that
(yes, yes, &lt;a href="https://www.djm.org.uk/posts/protect-yourself-from-non-obvious-dangers-curl-url-pipe-sh/" rel="noopener noreferrer" target="_blank"&gt;I
know&lt;/a&gt;...),
I was all set.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Antigravity CLI" height="692" loading="lazy" src="https://blog.davep.org/attachments/2026/05/20/agy.webp#centre" width="788" /&gt;&lt;/p&gt;
&lt;p&gt;Credit where it's due, when I ran it up it just worked. As in: I didn't need
to authorise again or anything like that; the fact that I'd set everything
up via the main application did seem to have done that job.&lt;/p&gt;
&lt;p&gt;After this though, it kind of went a little downhill. The first thing I
noticed was the set of models available was rather different from Gemini
CLI. I mean, okay, that's fair, I guess you expect things like that to
change, but in my inexperienced&lt;sup id="fnref:328-1"&gt;&lt;a class="footnote-ref" href="#fn:328-1"&gt;1&lt;/a&gt;&lt;/sup&gt; view of what these agentic tools offer,
it looked like all the options were a little more... pricey, perhaps?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Gemini CLI vs Antigravity CLI" height="264" loading="lazy" src="https://blog.davep.org/attachments/2026/05/20/old-vs-new.webp#centre" width="711" /&gt;&lt;/p&gt;
&lt;p&gt;Still, I'm sure that sensible defaults are chosen out of the box, so it
seemed like a good time to give this new tool a shot. I had &lt;a href="https://github.com/davep/blogmore/issues/500" rel="noopener noreferrer" target="_blank"&gt;a nice little
problem for it to work on&lt;/a&gt; so
that felt like a great test. It's hard to say for sure, but I feel like an
issue like that, with the right prompt, would have used up somewhere between
3% and 5% of the daily quota in Gemini CLI, using &lt;code&gt;Auto (Gemini 3)&lt;/code&gt;. That
was the default out of the box and, &lt;a href="https://blog.davep.org/2026/05/13/when-gemini-cli-gets-stuck.html"&gt;aside from tapping the models to try
and unstick them&lt;/a&gt;, I've never
really set it to anything else and the results have always been fine. With
all this in mind I set Antigravity to work. Given that there didn't seem to
be any sort of "Auto" option, I let it go with &lt;code&gt;Gemini 3.5 Flash (High)&lt;/code&gt;,
which is what it was set to out of the box.&lt;/p&gt;
&lt;p&gt;Yikes.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The model quotas" height="482" loading="lazy" src="https://blog.davep.org/attachments/2026/05/20/model-quota.webp#centre" width="542" /&gt;&lt;/p&gt;
&lt;p&gt;As I read that, and as I recall what happened, it took about 25 minutes to
get to a reasonable solution to the request, with me pushing back on a
couple of wild choices it made about how to change the code around. In doing
this it left me with just 20% of the quota free for the next four and a half
hours.&lt;/p&gt;
&lt;p&gt;Yikes.&lt;/p&gt;
&lt;p&gt;This is fine in this particular situation, where I'm conducting a long-term
experiment and often letting the tool run at reasonably self-contained
problems, in the background, while I get on with other more important
things. But if I were to try and use this, as I have Gemini CLI, for an
evening of sofa-hacking, refactoring lots of code or adding a handful of new
features... that's not going to be sustainable. Any such session is going to
grind to a halt pretty quickly. Presumably the intended solution here is
that I buy myself lots of "AI credits".&lt;/p&gt;
&lt;p&gt;&lt;img alt="I can always buy more credits" height="148" loading="lazy" src="https://blog.davep.org/attachments/2026/05/20/use-credits.webp#centre" width="502" /&gt;&lt;/p&gt;
&lt;p&gt;I will experiment more, and intend to try and work out what the point,
purpose and impact of each of the models are, as found in Antigravity.
Doubtless there's a smarter approach I can take where it'll cost less quota
for similar results. What is for sure though is that Antigravity CLI &lt;strong&gt;is
not&lt;/strong&gt; a drop-in replacement for Gemini CLI. It seems to be a different way
of working, with different models, and different considerations. Also with
&lt;a href="https://github.com/google-gemini/gemini-cli/discussions/27274" rel="noopener noreferrer" target="_blank"&gt;less openness
too&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's interesting to drop in on the &lt;a href="https://www.reddit.com/r/GeminiCLI/" rel="noopener noreferrer" target="_blank"&gt;Gemini CLI
subreddit&lt;/a&gt;, where &lt;a href="https://www.reddit.com/r/GeminiCLI/comments/1thweon/gemini_cli_is_transitioning_to_the_new/" rel="noopener noreferrer" target="_blank"&gt;the members seem to
be experiencing what the Copilot folk were a week or so
back&lt;/a&gt;.
People finding they're chewing through their quota in no time, only with the
added frustration of having to transition to a whole new application that
seems to be lacking some features they're used to.&lt;/p&gt;
&lt;p&gt;None of this is shocking to me -- although I'll admit that I thought the
Gemini CLI ride might last a wee while longer than it did -- nor, I'd hope,
to anyone else, but it continues to be fascinating to watch the squeeze
being applied all around this tool space. This is going to be an
increasingly worse time for anyone wanting to mess with agents for hobby
projects. The idea of a tool that lets you get unambitious projects done for
the price of a coffee or two, per month: that was a reasonable prospect.
When the real cost turns out to be similar to an actual utility bill for
your home... I know some people have expensive hobbies, but this would not
seem to be a rewarding one at the sorts of costs we're starting to look at.&lt;/p&gt;
&lt;p&gt;Once again, it's going to be interesting to see how engineering departments,
and AI-embracing companies as a whole, react, as they become more and more
invested in these third-party services, and less able to actually do things
themselves, while at the same time the suppliers of those services squeeze
them harder to try and make this adventure pay off.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:328-1"&gt;
&lt;p&gt;I say "inexperienced", but perhaps I'm being unfair to myself here.
While I'm not 100%, all in, fully-steeped in agentic lore, and even
though I've not been living this stuff full time for the past year or
so, I do feel I'm a good representation of someone with a long
background in the software development industry who is coming to these
tools with reasonable expectations.&amp;#160;&lt;a class="footnote-backref" href="#fnref:328-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/20/the-gemini-bait-and-switch.html"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <category term="Antigravity"/>
    <published>2026-05-20T17:04:26+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/20/goodbye-gemini-cli.html</id>
    <title>Goodbye Gemini CLI</title>
    <updated>2026-05-20T08:58:35+01:00</updated>
    <content type="html">&lt;p&gt;I just sat down at my desk and fired up Gemini CLI to get it to make a
change to &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;, and I see this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Goodbye Gemini CLI" height="740" loading="lazy" src="https://blog.davep.org/attachments/2026/05/20/goodbye-cli.webp#centre" width="900" /&gt;&lt;/p&gt;
&lt;p&gt;I've yet to actually look at Antigravity, so I know pretty much nothing
about it at this point. After a brief glance &lt;a href="https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/" rel="noopener noreferrer" target="_blank"&gt;at the link that was
given&lt;/a&gt;
it &lt;em&gt;seems&lt;/em&gt; like it's a positive change, perhaps. Honestly, I'm not sure. But
that's kind of moot, I don't really have a choice. Within a month Gemini CLI
is going to stop working anyway.&lt;/p&gt;
&lt;p&gt;This is yet another reminder that, while plenty of folk are pushing these
tools as &lt;em&gt;the&lt;/em&gt; answer to the "problem" of software development, they're not
really stable tools, it's not really a stable market, and, to some degree,
if you fully rely on these tools, you're constantly at the mercy of the
whims of some other company.&lt;/p&gt;
&lt;p&gt;I'm glad I have a project where I'm forcing reliance on them as an
experiment, so I can see and experience this first-hand, but I'd be very
concerned for someone who's fully bought into them.&lt;/p&gt;
&lt;p&gt;Perhaps there's a market here for a "Killed by AI" website, much like
&lt;a href="https://killedbygoogle.com/" rel="noopener noreferrer" target="_blank"&gt;Killed by Google&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;Or, maybe I'm being unfair here; it could be that this is more akin to
Google solving the chat problem by constantly moving people from one chat
application to another, while also having chat abilities in all sorts of
other products...&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/20/goodbye-gemini-cli.html"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <category term="Antigravity"/>
    <published>2026-05-20T08:58:35+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/18/the-highs-and-the-lows.html</id>
    <title>The highs and the lows</title>
    <updated>2026-05-18T08:34:29+01:00</updated>
    <content type="html">&lt;p&gt;Over the weekend I read a comment, I think it was on Hacker News, where
someone said they were having fun building things using AI. This was in
response to someone saying that using AI took the fun out of programming. In
their reply, the person qualified their answer with something along the
lines of &lt;em&gt;"the highs are higher and the lows are lower"&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I think I agree.&lt;/p&gt;
&lt;p&gt;My first ever exposure to any sort of computer was a Sinclair ZX80 that my
maths teacher brought into school. After a class he plugged it in and let me
and a friend take a look. To this day I still remember looking in the
manual, looking at the tutorial, and at some point typing...&lt;/p&gt;
&lt;p&gt;&lt;img alt="PRINT 1+1" height="107" loading="lazy" src="https://blog.davep.org/attachments/2026/05/18/one-plus-one.webp#centre" width="237" /&gt;&lt;/p&gt;
&lt;p&gt;When I hit &lt;kbd&gt;NEW LINE&lt;/kbd&gt; and a &lt;code&gt;2&lt;/code&gt; appeared on the screen I was
thrilled, I was hooked. I'd typed something that &lt;strong&gt;appeared on a TV screen&lt;/strong&gt;
and then I did something that &lt;strong&gt;made the answer appear on the TV screen&lt;/strong&gt;.
This felt like magic.&lt;/p&gt;
&lt;p&gt;I've been hooked on writing code ever since.&lt;/p&gt;
&lt;p&gt;In that time the highs have been high, and the lows have been low, but I
think it's fair to say that I've been doing this for long enough (it's now
45 or 46 years since I typed that first instruction) that things have
settled down. I still get a thrill when writing code, and I still get fed up
with it from time to time, but the distance between the two isn't what it
once was.&lt;/p&gt;
&lt;p&gt;Which brings me back to the comment I read: I think I can safely say that,
while properly experimenting with agents, while building
&lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt; to test this approach out, I have
been through a period of higher highs and lower lows when it comes to how I
feel about the code and the project itself. When I &lt;a href="https://blog.davep.org/2026/02/20/five-days-with-copilot.html"&gt;kicked off
development&lt;/a&gt; it was genuinely
thrilling to have gone from an empty repository to a comprehensively-working
initial version in just a matter of hours. Likewise it was thrilling to have
gone from nothing to &lt;a href="https://blog.davep.org/2026/02/19/a-new-engine.html"&gt;rebuilding this blog with the
tool&lt;/a&gt; in just a few days. It would be a lie
to suggest that it wasn't fun and exciting to see the result.&lt;/p&gt;
&lt;p&gt;But, as I wrote back then, I was also very mindful of how empty the process
felt at times, how I missed the whole "flow state" connection to building
out the application. There have also been many moments along the way, which
I've documented at times on this blog, where I've felt the project was
getting stuck down a dead-end with respect to how the code was going.&lt;/p&gt;
&lt;p&gt;And then there's all the times Copilot and/or Gemini CLI just plain stopped
getting stuff done.&lt;/p&gt;
&lt;p&gt;Given this -- given the highs especially -- I can see why some people get
totally hooked, go all in, get consumed by the illusion of how powerful
these tools are. I can see why they'd buy into and embrace the mindset that
trots out the AI-equivalent of the crypto-hype &lt;em&gt;"stay poor"&lt;/em&gt; retort to those
who display any level of scepticism.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/18/the-highs-and-the-lows.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Hacker News"/>
    <category term="history"/>
    <published>2026-05-18T08:34:29+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/17/busy-doing-nothing.html</id>
    <title>Busy doing nothing</title>
    <updated>2026-05-17T10:08:03+01:00</updated>
    <content type="html">&lt;p&gt;The first company I worked for full-time had two offices. One in the south
of England, another in the north. Despite being a northern lad, I'd somehow
found myself working in the southern office. While the company used a few
languages, there was a split between the two offices, mostly driven by the
fact that the northern office was more minicomputer-based (lots of DEC stuff
as I recall), whereas at our office it was more PC-based (we were an
&lt;a href="https://en.wikipedia.org/wiki/Apricot_Computers" rel="noopener noreferrer" target="_blank"&gt;Apricot&lt;/a&gt; dealership,
amongst other things). At our office, the predominant language was
&lt;a href="https://en.wikipedia.org/wiki/Clipper_(programming_language)" rel="noopener noreferrer" target="_blank"&gt;Clipper&lt;/a&gt;
(later to be called CA-Clipper).&lt;/p&gt;
&lt;p&gt;At one point, at the other office, they hired someone to start doing Clipper
coding up there too, and he was handed his first project, to add a new
report to an existing system. After around three weeks, he just didn't turn
up for work one day, called in to say he'd quit (or so I was told).
Meanwhile, the work he had done didn't seem to be working. If someone took
the newly-compiled system and ran the new report, nothing happened.&lt;/p&gt;
&lt;p&gt;When the code was looked at, it became clear why. The new module had one
line of code. Well, not one line of code exactly: it had a one-line comment.&lt;/p&gt;
&lt;div class="highlight" data-lang="clipper"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;* This is too hard. I can&amp;#39;t do this.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That was it. He'd spent those weeks appearing to work on the requirement,
but never produced a single line of actual code.&lt;/p&gt;
&lt;p&gt;I felt really bad for the guy. He'd somehow managed to make it through the
interview, somehow managed to convince others, and himself, that he was
capable of working with Clipper and writing code (probably made easier by
the fact that the office in question wasn't a "Clipper shop"). But when it
came to actually getting on with a job, he'd been unable to get it done
(and, apparently, had felt unable to ask anyone around him for help, which
probably says a lot about that office and the industry at the time&lt;sup id="fnref:340-1"&gt;&lt;a class="footnote-ref" href="#fn:340-1"&gt;1&lt;/a&gt;&lt;/sup&gt;).&lt;/p&gt;
&lt;p&gt;I bring this up because I was reminded of this story when I was tinkering
with &lt;a href="https://blog.davep.org/tag/gemini/"&gt;Gemini&lt;/a&gt; last night. While working on the &lt;a href="https://github.com/davep/blogmore/pull/492" rel="noopener noreferrer" target="_blank"&gt;optimised
images PR&lt;/a&gt;, towards the end of
the session, I asked it to make a particular change. It then started
"thinking", and after a couple of minutes appeared to get to work on the
problem. It kept printing, scrubbing out and printing again, lines of text
of what it was apparently doing. This went on for something like five
minutes. Eventually it announced that the work had been done, explained what
it had changed, and how it had implemented the requirement.&lt;/p&gt;
&lt;p&gt;I flipped to another terminal to test out the work and... no changes. Zero
changes. Nothing to diff, nothing to commit.&lt;/p&gt;
&lt;p&gt;I flipped back to the CLI app, mentioned that nothing had changed, and it
then very quickly made some edits; nothing spectacular, a 14-line diff
affecting five lines (to start with).&lt;/p&gt;
&lt;p&gt;This is the first time I've seen this, and I guess yet another thing I need
to keep an eye out for. Of course I would notice if I asked for some work to
be done and it wasn't done (I did), but it feels like another method via
which this "productivity tool" can make you less productive.&lt;/p&gt;
&lt;p&gt;If you give me the under-qualified, solution-paralysed, entry-level
developer who doesn't know how to proceed, I can help them. Their current
inability to actually bash on the keyboard and make code appear isn't the
problem here. Giving them a tool that will busy-work for five minutes and
produce nothing isn't going to help them, neither will things improve if
they're given a tool that &lt;em&gt;does&lt;/em&gt; emit all the code. Removing the human
element is going to remove safety, growth and also domain knowledge. I feel
it's going to rot software engineering departments from within, if handled
badly.&lt;/p&gt;
&lt;p&gt;Watching people talk about agents as if they're the solution, and that
writing code is now a solved problem, really troubles me. I won't question
the idea that it can be a very useful tool -- goodness knows I've found it
useful recently -- but I do question the common assertion that it finally is
a &lt;a href="https://en.wikipedia.org/wiki/No_Silver_Bullet" rel="noopener noreferrer" target="_blank"&gt;silver bullet&lt;/a&gt;. I find
this to be lazy, dangerous and harmful thinking.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:340-1"&gt;
&lt;p&gt;Because of course we're so much better as an industry these days.&amp;#160;&lt;a class="footnote-backref" href="#fnref:340-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/17/busy-doing-nothing.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <published>2026-05-17T10:08:03+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/16/gemini-cli-vs-github-copilot-the-result.html</id>
    <title>Gemini CLI vs GitHub Copilot (the result)</title>
    <updated>2026-05-16T15:00:23+01:00</updated>
    <content type="html">&lt;p&gt;Following on from &lt;a href="https://blog.davep.org/2026/05/16/gemini-cli-vs-github-copilot-redux.html"&gt;this morning's initial
experiment&lt;/a&gt;, I think
I'm settling on a winner. Rather than be annoying and have you scroll to the
bottom to find out: it's Gemini CLI. Here's how I found the process played
out, and why I'm settling for one over the other.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id="gemini-cli"&gt;Gemini CLI&lt;a aria-label="Link to this heading" class="heading-anchor" href="#gemini-cli"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Initially this was an absolute mess. After letting it initially work on the
problem, the resulting code didn't even really run. The first go, and the
three follow-up prompt/result cycles that followed, all resulted in code
that had runtime errors. I'm pretty sure it didn't even bother to try and do
any adequate testing. This is odd given I've generally seen it do an okay
job when it comes to writing and running tests.&lt;/p&gt;
&lt;p&gt;Once I had the code in a stable state, with all type checking, linting and
testing passing, it still didn't work. No matter how I tried to use the new
facility it just didn't make a difference. No images were optimised. In the
end I dived into the code, with the help of its attempt at debugging (it
added &lt;code&gt;print&lt;/code&gt; calls to try and get to the bottom of things -- how very
human!), diagnosed what I thought was the issue (it was looking in the wrong
location for the files to optimise), told it my hypothesis and let it check
if I was right. It concluded I was and fixed the problem.&lt;/p&gt;
&lt;p&gt;Since then I've had a working implementation of &lt;a href="https://github.com/davep/blogmore/issues/490" rel="noopener noreferrer" target="_blank"&gt;the initial
plan&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once that was in place it's been a pretty smooth journey. I've asked it
questions about the implementation, had my concerns set to rest, had some
concerns addressed and fixed, improved some things here and there, added new
features, etc.&lt;/p&gt;
&lt;p&gt;All of this has left me with 18% of my daily quota used up. While I think
this is the highest I've ever got while using Gemini CLI, it still feels
like I got a lot of things done for not a lot of quota use.&lt;/p&gt;
&lt;h2 id="github-copilot"&gt;GitHub Copilot&lt;a aria-label="Link to this heading" class="heading-anchor" href="#github-copilot"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Initially I thought this had managed to one-shot the problem. Once it had
finished its initial work the code ran without incident and produced all the
optimised files. Or so I thought. Doing a little more testing, though, it
became clear it was only optimising a subset of the images &lt;em&gt;and&lt;/em&gt; it didn't
seem to be producing the actual HTML to use the images.&lt;/p&gt;
&lt;p&gt;On top of this it didn't even follow the full plan that was laid out in the
issue it was assigned. For example: once I'd got it doing the main part of
the work, it became apparent that it had pretty much ignored the whole idea
of using a cache to speed this process up. I had to remind it to do this.&lt;/p&gt;
&lt;p&gt;At one point I switched from the in-PR web interaction with Copilot, and
used the local CLI instead. When I ran that up it warned me that I was
already 50% of the way through some sort of rate limit and this wouldn't
reset for another 3 hours. I think I was about 40 minutes into letting it
try and do the work at this point.&lt;/p&gt;
&lt;p&gt;After a bit more testing and follow-up prompts, I got to a point where I had
something that looked like it was working; albeit in a slightly different
way from how Gemini CLI did it (the Copilot approach was writing the
optimised images out to the &lt;code&gt;extras&lt;/code&gt; directory, mixing them in with my own
images; Gemini opted for having a separate directory for optimised images
within the &lt;code&gt;static&lt;/code&gt; hierarchy).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;At this point I will admit to &lt;em&gt;not&lt;/em&gt; having carefully reviewed the code of
either agent; that's a job still to do. But while Gemini got off to a very
rocky start, with a bit of guidance it seemed to arrive at an implementation
I'm happy with, and one that seems to be working as intended. While it
didn't anticipate all the edge cases, when I asked about them it easily
found and implemented solutions for them. Moreover, the fact that I could do
all of this and confidently know the "cost" made a huge difference. Copilot
seems to generally approach this like a quota or rate limit should be a
lovely surprise that will destroy your flow; Gemini has it there and in
front of you, all the time.&lt;/p&gt;
&lt;p&gt;As for the general idea that I'm working on: I &lt;em&gt;think&lt;/em&gt; I'm going to
implement it. Weirdly I'm slightly nervous about building the blog such that
it won't be using the images &lt;strong&gt;I&lt;/strong&gt; created, but I also recognise that that's
a little irrational. Meanwhile I'm very curious about the impact this might
have &lt;a href="https://blog.davep.org/2026/04/16/i-should-use-webp.html"&gt;on the PageSpeed measurement&lt;/a&gt; of
the blog. While it's far from horrific, image size optimisation and size
declaration seem to be fairly high on the things that are impacting the
performance score (currently sat at 89 for the front page of the blog, as I
type this).&lt;/p&gt;
&lt;p&gt;The other thing that gives me pause for thought about merging this in, and
then subsequently using it, is that I've &lt;em&gt;just&lt;/em&gt; &lt;a href="https://blog.davep.org/2026/05/15/converted-to-webp.html"&gt;finished migrating all
images to webp&lt;/a&gt;, and so saving a lot of
space in the built version of the blog. Generating all the responsive sizes
of the images eats that up again. With this feature off, the built version
of the blog stands at about 84MB; with it on, this rises to 133MB. That
extra 49MB more than eats up the 24MB saving I made earlier.&lt;/p&gt;
&lt;p&gt;On the other hand: storage is a thing for GitHub to worry about, what I'm
worrying about here, and aiming to improve, is the reader's experience.&lt;/p&gt;
&lt;p&gt;I'm going to sit on this for a short while and play around with it, at least
until I get impatient and say "what the hell" and run with it.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/16/gemini-cli-vs-github-copilot-the-result.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <published>2026-05-16T15:00:23+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/16/gemini-cli-vs-github-copilot-redux.html</id>
    <title>Gemini CLI vs GitHub Copilot (redux)</title>
    <updated>2026-05-16T09:30:23+01:00</updated>
    <content type="html">&lt;p&gt;Given I'm &lt;a href="https://blog.davep.org/2026/05/13/the-copilot-bait-and-switch.html"&gt;almost certainly&lt;/a&gt;
going to drop GitHub Copilot starting next month, I'm using Gemini CLI more
and more for &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;. Yesterday evening, I
used it to plan out an idea for a change to the application. Now that I've
&lt;a href="https://blog.davep.org/2026/05/15/converted-to-webp.html"&gt;migrated all images to WebP&lt;/a&gt;, I thought
it might be interesting to look at the idea of having a responsive approach
to images. This is something I don't know a whole lot about (never having
needed to bother with it before), but it also happens that I need to read up
on this anyway for something related to the day job; given this, it felt
like a good time to experiment.&lt;/p&gt;
&lt;p&gt;Together with Gemini CLI &lt;a href="https://github.com/davep/blogmore/issues/490" rel="noopener noreferrer" target="_blank"&gt;a plan was
created&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This morning, over second coffee, I've kicked off the job of implementing it
and, honestly, Gemini CLI is really struggling. It "implemented" the change
pretty quickly, within minutes, but it just plain didn't work. Since then
I've had it iterate over the issue four times and now it's struggling to
make it work at all. It's still beavering away on this as I type, and
consuming daily quota at a fair rate too.&lt;/p&gt;
&lt;p&gt;So, while I still have GitHub Copilot, this feels like a good point to play
them off against each other at least one more time. Having saved the plan
Gemini wrote last night as an issue, &lt;a href="https://github.com/davep/blogmore/pull/491" rel="noopener noreferrer" target="_blank"&gt;I've assigned it to
Copilot&lt;/a&gt; (using Claude Sonnet
4.6). As I type this, I have Gemini racing to get this working in a terminal
window behind Emacs, meanwhile there's Claude doing its thing in GitHub's
cloud.&lt;/p&gt;
&lt;p&gt;It'll be interesting to see if Copilot manages to one-shot this, for sure
Gemini is far off a one-shot implementation.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/16/gemini-cli-vs-github-copilot-redux.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <published>2026-05-16T09:30:23+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/14/gemini-is-kind-of-messy.html</id>
    <title>Gemini is kind of messy</title>
    <updated>2026-05-14T08:25:23+01:00</updated>
    <content type="html">&lt;p&gt;As I've mentioned a few times recently, I'm using Google's &lt;a href="https://blog.davep.org/tag/gemini/"&gt;Gemini
CLI&lt;/a&gt; more at the moment; in part because I have a Gemini Pro
account so it makes sense to use it, but also &lt;a href="https://blog.davep.org/2026/05/13/the-copilot-bait-and-switch.html"&gt;in anticipation of dropping
anything to do with Copilot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While I've had some troubles with it -- as can be seen
&lt;a href="https://blog.davep.org/2026/05/03/a-stroppy-agent.html"&gt;here&lt;/a&gt;,
&lt;a href="https://blog.davep.org/2026/05/12/the-other-unreliable-buddy.html"&gt;here&lt;/a&gt; and
&lt;a href="https://blog.davep.org/2026/05/13/when-gemini-cli-gets-stuck.html"&gt;here&lt;/a&gt; for example -- I'm
mostly having an okay time. The code it writes isn't too bad, and while it
seems to need a little more direction and overseeing than I've been used to
while using Copilot/Claude, it generally seems to arrive at sensible
solutions for the problems I'm throwing at it&lt;sup id="fnref:293-1"&gt;&lt;a class="footnote-ref" href="#fn:293-1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;One difference with working with Copilot CLI that I &lt;em&gt;have&lt;/em&gt; noticed, however,
is that Gemini doesn't seem to care for cleaning up after itself. When faced
with a problem it'll often write a test program or two, perhaps even create
a subdirectory to hold some test data, run the tests and be sure about the
outcome. This is good to see. It's not unusual for me to do this myself (or
at least in the REPL anyway). But it really doesn't seem to care to actually
clean up those tests. A handful of times now I've had it leave those files
and directories kicking around. I've even said to it &lt;em&gt;"please clean up your
test files"&lt;/em&gt; and it's gone right ahead and done so, which suggests it
"knows" what it did and what it should do.&lt;/p&gt;
&lt;p&gt;This also feels like a new source of mess for all the people who commit
their executables and the like to their repositories. That should be fun.&lt;/p&gt;
&lt;p&gt;The thing I don't know or understand, at least at the moment, is if this is
down to the CLI harness itself, or the choice of model, or a combination of
both, or something else. I'm curious to know more.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:293-1"&gt;
&lt;p&gt;There is a weird thing I'm seeing, which I want to try and properly
capture at some point, where it'll start tinkering with unrelated code,
I'll undo the change, it'll throw it back in the next go, I'll undo,
rinse, repeat...&amp;#160;&lt;a class="footnote-backref" href="#fnref:293-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/14/gemini-is-kind-of-messy.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <published>2026-05-14T08:25:23+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/13/when-gemini-cli-gets-stuck.html</id>
    <title>When Gemini CLI gets stuck</title>
    <updated>2026-05-13T20:27:54+01:00</updated>
    <content type="html">&lt;p&gt;Another evening, and &lt;a href="https://blog.davep.org/2026/05/12/the-other-unreliable-buddy.html"&gt;another period of Gemini CLI getting stuck
thinking&lt;/a&gt;. So this time I
thought I'd try something: cancel it while it was thinking and change the
model.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Gemini Thinking..." height="342" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/thinking.webp#centre" width="702" /&gt;&lt;/p&gt;
&lt;p&gt;I was working &lt;a href="https://github.com/davep/blogmore/pull/483" rel="noopener noreferrer" target="_blank"&gt;on something new for
BlogMore&lt;/a&gt; and, sure enough,
after a wee while, we got stuck in &lt;em&gt;"Thinking..."&lt;/em&gt; mode. So I hit
&lt;kbd&gt;Escape&lt;/kbd&gt; and asked to pick a different model. I chose to pick
manually, and went with &lt;code&gt;gemini-3.1-pro-preview&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Picking the model" height="302" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/pick-the-model.webp#centre" width="616" /&gt;&lt;/p&gt;
&lt;p&gt;I then literally asked that it carry on where it left off...&lt;/p&gt;
&lt;p&gt;&lt;img alt="Carry on" height="246" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/select-model.webp#centre" width="536" /&gt;&lt;/p&gt;
&lt;p&gt;...and it did! It worked. No more sitting around thinking for ages.&lt;/p&gt;
&lt;p&gt;Watching the quota after doing this, it looks like the model I was using ate
quota faster, but that was worth it given I've never come close to hitting
full quota with Gemini CLI.&lt;/p&gt;
&lt;p&gt;Once the immediate job was done, I went back to auto and it worked for a
bit, only to get stuck thinking again. I repeated this process and it did
the trick a second time. From now on I'm going to use this approach.&lt;/p&gt;
&lt;p&gt;It does, again, highlight how unreliable these tools are, but at least I've
found a workaround that works for now.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/13/when-gemini-cli-gets-stuck.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <published>2026-05-13T20:27:54+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/13/the-copilot-bait-and-switch.html</id>
    <title>The Copilot bait and switch</title>
    <updated>2026-05-13T08:31:27+01:00</updated>
    <content type="html">&lt;p&gt;Well, it's here: GitHub's tool to let you see how much better off you're
going to be under the new Copilot billing system that comes in next month.
It's... something.&lt;/p&gt;
&lt;p&gt;But let's set the background first. I'm here (in &lt;a href="https://blog.davep.org/tag/copilot/"&gt;Copilot&lt;/a&gt;
usage space) as an observer, spending time on an experiment that started
with the free pro tier and then transitioned into the &lt;em&gt;"okay, I'll play
along for $10 a month, the &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;tool I'm building is fun to work on and is
useful to me&lt;/a&gt;"&lt;/em&gt; phase. I doubted it would last
forever -- the price was obviously too good to be true for too long -- but I
wasn't expecting it to collapse quite so soon and in quite such a
spectacular way.&lt;/p&gt;
&lt;p&gt;When GitHub &lt;a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/" rel="noopener noreferrer" target="_blank"&gt;announced the move to usage-based
billing&lt;/a&gt;
I was curious to see if I'd be better off or worse off. It was hard to call
really. My use of Copilot is sporadic, and as BlogMore has started to settle
down and reach a state approaching feature-saturation the need to do heavy
work on it has reduced. I did use it a fair bit last month, but that was
more in tinkering and experimenting mode rather than full development
mode&lt;sup id="fnref:313-1"&gt;&lt;a class="footnote-ref" href="#fn:313-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, so it's probably a good measure.&lt;/p&gt;
&lt;p&gt;Checking the details on GitHub, it looks like I used a touch under 1/3 of my
premium requests.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A table of my premium requests for April 2026" height="723" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/requests-table.webp#centre" width="683" /&gt;&lt;/p&gt;
&lt;p&gt;It also looks like the usage came in a couple of bursts lasting a few days,
with a pretty flat period in the middle of the month.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Cumulative use for April" height="409" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/april-usage.webp#centre" width="681" /&gt;&lt;/p&gt;
&lt;p&gt;So, technically, GitHub won. I paid them $10 for 300 premium requests, I
left a touch over 2/3 unused. I think it's fair to suggest that I'm a pretty
lightweight user, even when I have a project under active development.&lt;/p&gt;
&lt;p&gt;This is where the new usage-based preview tool comes in. Launched yesterday,
it lets you take your existing usage stats and see how much it would have
&lt;em&gt;really&lt;/em&gt; cost you.&lt;/p&gt;
&lt;p&gt;The app itself comes over as being hastily spat out with an agent and little
communication between responsible teams. You'd &lt;em&gt;think&lt;/em&gt; you just press a
button when viewing some historical usage figures and get a display that
shows you what it would cost under the new approach.&lt;/p&gt;
&lt;p&gt;You'd think.&lt;/p&gt;
&lt;p&gt;Nope. First you generate your report for a particular month. Then have to
ask for it to be &lt;strong&gt;emailed to you as a CSV&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Requesting the email" height="236" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/email-report.webp#centre" width="499" /&gt;&lt;/p&gt;
&lt;p&gt;Even that part isn't super reliable. When I tried it last night it took a
wee while to turn up, and that was after about 10 attempts where I got an
error message saying it couldn't generate the report. This morning I tried
again and I've yet to see the email, 30 minutes later&lt;sup id="fnref:313-2"&gt;&lt;a class="footnote-ref" href="#fn:313-2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Having done that you click through to another page/app where you have to
upload the CSV, to GitHub, that GitHub just sent you in an email. Brilliant.
It then gives you the good news.&lt;/p&gt;
&lt;p&gt;So what is my 1/3 use of the premium request allowance going to save me
under the new approach to billing?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Such a good deal" height="541" loading="lazy" src="https://blog.davep.org/attachments/2026/05/13/the-new-price.webp#centre" width="571" /&gt;&lt;/p&gt;
&lt;p&gt;Amazing. I especially like the part where they spin it as: if I spent
$39/month with them I would save money!&lt;/p&gt;
&lt;p&gt;I guess I should take comfort that I'm not &lt;a href="https://www.reddit.com/r/GithubCopilot/comments/1tbfkui/ill_just_leave_this_here/" rel="noopener noreferrer" target="_blank"&gt;that one Reddit user whose $39
April would really have cost them almost
$6,000&lt;/a&gt;&lt;sup id="fnref:313-3"&gt;&lt;a class="footnote-ref" href="#fn:313-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Watching this journey has been wild. The free Pro as a taster to get me onto
$10/month I can go with, that's fair enough. For the longest time I never
even paid it any attention. But watching GitHub give it to so many people,
and especially so many students, and then watching them do &lt;em&gt;shocked Pikachu&lt;/em&gt;
when it cost them an arm and a leg and probably caused the degradation of
the performance of their systems... who could possibly have seen this
coming? &lt;a href="https://en.wikipedia.org/wiki/Tragedy_of_the_commons" rel="noopener noreferrer" target="_blank"&gt;Impossible to
predict&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Back when I first wrote about my initial impressions of working with Copilot
I wondered &lt;a href="https://blog.davep.org/2026/02/20/five-days-with-copilot.html#conclusion"&gt;in the
conclusion&lt;/a&gt; if I'd
transition to a paying version of Copilot. I obviously did. At $10/month it
was a very affordable tinker toy that gave me a new dimension to the hobby
side of my love of creating things with code. But the prospect of paying
$39/month for something in the region of 1/3 of requests that I had before:
nah, I'm not into that.&lt;/p&gt;
&lt;p&gt;It looks like this month will be the last month I keep a Copilot
subscription. BlogMore will carry on being developed, I'll probably
transition to leaning on &lt;a href="https://blog.davep.org/tag/gemini/"&gt;Gemini&lt;/a&gt; CLI more (&lt;a href="https://blog.davep.org/2026/05/11/speeding-up-blogmore.html"&gt;as I have been
the last week anyway&lt;/a&gt;), and also
start to get my hands dirty with the code more too.&lt;/p&gt;
&lt;p&gt;This feels like one of the early signs of the bait and switch that the AI
suppliers have been building up all along. Experimenting and better
understanding how and why people use these tools has been seriously useful,
and I can't help but feel that I accidentally started at just the right
moment. Watching this happen, with actual experience of what's going on, is
very educational. It's going to be super interesting to see if this same
stunt gets pulled on a bigger scale, with all the companies that
uncritically embraced AI at every level of their organisation.&lt;/p&gt;
&lt;p&gt;It's going to be especially interesting to watch the AI leaders in those
companies to see how they spin this, if and when the real costs are more
widely applied.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:313-1"&gt;
&lt;p&gt;Is my recollection. I should probably &lt;a href="https://blogmore.davep.dev/changelog/" rel="noopener noreferrer" target="_blank"&gt;review the
ChangeLog&lt;/a&gt; and see what I
actually did add in April.&amp;#160;&lt;a class="footnote-backref" href="#fnref:313-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:313-2"&gt;
&lt;p&gt;Yes I checked spam.&amp;#160;&lt;a class="footnote-backref" href="#fnref:313-2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:313-3"&gt;
&lt;p&gt;In part because yikes, but also in part because at least I'm not the
reason this is happening, unlike them.&amp;#160;&lt;a class="footnote-backref" href="#fnref:313-3" title="Jump back to footnote 3 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/13/the-copilot-bait-and-switch.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Business"/>
    <category term="Copilot"/>
    <category term="GitHub"/>
    <published>2026-05-13T08:31:27+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/12/the-other-unreliable-buddy.html</id>
    <title>The other unreliable buddy</title>
    <updated>2026-05-12T08:20:25+01:00</updated>
    <content type="html">&lt;p&gt;Having had &lt;a href="https://blog.davep.org/2026/05/09/an-unreliable-buddy.html"&gt;Copilot crash out the other
day&lt;/a&gt;, while working on the
&lt;a href="https://blogmore.davep.dev/linting/" rel="noopener noreferrer" target="_blank"&gt;linter&lt;/a&gt; for
&lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;, I decided to lean into &lt;a href="https://geminicli.com/" rel="noopener noreferrer" target="_blank"&gt;Gemini
CLI&lt;/a&gt; a little more and see how that got on.&lt;/p&gt;
&lt;p&gt;When I first tried it out, &lt;a href="https://blog.davep.org/2026/05/05/and-then-there-were-three.html"&gt;a week
back&lt;/a&gt;, I found it worked fairly
well but could be rather slow at times. On the whole though, I found it easy
enough to work with; the results weren't too bad, even if it could &lt;a href="https://blog.davep.org/2026/05/03/a-stroppy-agent.html"&gt;throw
out some mildly annoying code at times&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yesterday evening though, because of the failure of Copilot, I decided to go
just with Gemini and work on the &lt;a href="https://blog.davep.org/2026/05/11/speeding-up-blogmore.html"&gt;problem of speeding up
BlogMore&lt;/a&gt;. This worked really well. I
found that it followed instructions well&lt;sup id="fnref:335-1"&gt;&lt;a class="footnote-ref" href="#fn:335-1"&gt;1&lt;/a&gt;&lt;/sup&gt; when given them, and also did a
good job of applying what it was told, constantly, without needing to be
told again. I actually found I had a bit of a flow going (in the minimal way
that you can get any sort of flow going when you're not hand-coding).&lt;/p&gt;
&lt;p&gt;Using it, I tackled all the main bottlenecks in BlogMore and got things
working a lot faster (at this point it's generating a site in about 1/4 of
the time it used to take). By the time that work was done, I wanted to do
some last tidying up.&lt;/p&gt;
&lt;p&gt;This was where it suddenly got unreliable. I asked it a simple question, not
even tasking it with something to do, and it went into "&lt;em&gt;Thinking...&lt;/em&gt;" mode
and never came back out of it. I seem to remember I gave it 10 minutes and
then cancelled the request.&lt;/p&gt;
&lt;p&gt;After that I tried again with a different question, having quit the program
and started it again with &lt;code&gt;--resume&lt;/code&gt;. This time I asked it a different
question and the same thing happened. I hit cancel again and then, a moment
later, &lt;em&gt;finally&lt;/em&gt; got an answer to the previous question.&lt;/p&gt;
&lt;p&gt;From this point onwards I could barely ever get a reply out of it. I even
tried quitting and starting up again without &lt;code&gt;--resume&lt;/code&gt;, only for the same
result.&lt;/p&gt;
&lt;p&gt;A quick search turns up reports similar to this issue on Reddit, Google's
support forums and &lt;a href="https://github.com/google-gemini/gemini-cli/issues/25520" rel="noopener noreferrer" target="_blank"&gt;on
GitHub&lt;/a&gt;. It looks
like I'm not alone in running into this.&lt;/p&gt;
&lt;p&gt;This here is one of the things that concerns me about the idea of ever
adopting agents as the primary tool for getting code written: the
unreliability of their availability, and so the resulting inconsistency of
the output. It feels like any perceived win in terms of getting the code
written is going to be lost in the frustration of either waiting and trying
again when it just gives up playing along, or in running from one agent to
another, hoping you find the one that is capable of working with you at that
given moment.&lt;/p&gt;
&lt;p&gt;Meanwhile folk talk like it's &lt;em&gt;the&lt;/em&gt; solution to the problem of software
development. It's especially concerning when those folk are in "engineering
leadership" or a position with a similar name. When they talk like this they
are either displaying a lack of foresight, or betraying a lack of care for
the craft they are supposed to represent (amongst &lt;a href="https://en.wikipedia.org/wiki/Peter_principle" rel="noopener noreferrer" target="_blank"&gt;other
reasons&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;It's very timely that &lt;a href="https://hachyderm.io/@robpike/116557975987213548" rel="noopener noreferrer" target="_blank"&gt;this
post&lt;/a&gt; from
&lt;a href="https://en.wikipedia.org/wiki/Rob_Pike" rel="noopener noreferrer" target="_blank"&gt;Rob Pike&lt;/a&gt; popped up in my feed this
morning:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Although trained in physics, I worked in the computing industry with pride
and purpose for over 40 years. And now I can do nothing but sit back and
watch it destroy itself for no valid reason beyond hubris (if I'm being
charitable).&lt;/p&gt;
&lt;p&gt;Ineffable sadness watching something I once loved deliberately lose its
soul.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yup.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:335-1"&gt;
&lt;p&gt;Albeit I sense it pays little to no attention to &lt;code&gt;AGENTS.md&lt;/code&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:335-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/12/the-other-unreliable-buddy.html"/>
    <category term="AI"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <category term="leadership"/>
    <category term="quote"/>
    <published>2026-05-12T08:20:25+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/10/an-argument-with-gemini.html</id>
    <title>An argument with Gemini</title>
    <updated>2026-05-10T09:39:49+01:00</updated>
    <content type="html">&lt;p&gt;At the moment I'm working on a linting command for
&lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;. &lt;a href="https://blog.davep.org/2026/05/09/an-unreliable-buddy.html"&gt;Having given up on Copilot/Claude
for this&lt;/a&gt;, I've been having quite a
bit of success with Gemini CLI. But while doing this, I've noticed some odd
things with it. It does have this habit of cargo-culting some changes, or
just rewriting code that doesn't need it.&lt;/p&gt;
&lt;p&gt;For example, the tests for the new linting tool: it keeps adding &lt;code&gt;import
pytest&lt;/code&gt; near the top of the test file &lt;em&gt;despite the fact that &lt;code&gt;pytest&lt;/code&gt;
doesn't get used anywhere in the code&lt;/em&gt;. Every time, I'll remove it, every
time it adds more tests, it'll add it back.&lt;/p&gt;
&lt;p&gt;Another thing I've noticed is it seems to be obsessed with adding
indentation to empty lines. So, if you've got a line of code indented 8
spaces, then an empty line, then another line of code indented 8 spaces,
&lt;em&gt;it'll add 8 spaces on that empty line&lt;/em&gt;. That sort of thing annoys the hell
out of me&lt;sup id="fnref:324-1"&gt;&lt;a class="footnote-ref" href="#fn:324-1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;But the worst thing I just ran into was this. It had written this bit of
code:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;lint_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SiteConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Convenience function to run the linter.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        site_config: The site configuration.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        0 if no errors, 1 if errors were found.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;linter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Linter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;linter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On the surface this seems fine: a function that hides just a little bit of
detail while providing a simple function interface to a feature. But that
use of a variable to essentially "discard" it the next line... nah. I
dislike that sort of thing. The code can be just a little more elegant. So
seeing this I edited it to be (removing the docstring for the purposes of
this post):&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;lint_site&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SiteConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Linter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Nice and tidy.&lt;/p&gt;
&lt;p&gt;I then had Gemini work on something else in the linting code. What did I see
towards the end of the diff? This!&lt;/p&gt;
&lt;p&gt;&lt;img alt="A sneaky edit" height="144" loading="lazy" src="https://blog.davep.org/attachments/2026/05/10/sneaky-edit.webp#centre" width="512" /&gt;&lt;/p&gt;
&lt;p&gt;Sneaky little shit!&lt;/p&gt;
&lt;p&gt;Now, sure, the idea is you review all changes before you run with them, but
knowing that it's likely that any given change might rewrite parts of the
code that aren't related to the problem at hand adds a lot more overhead,
and I wonder how often people using these tools even bother.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:324-1"&gt;
&lt;p&gt;I've seen some IDEs do that on purpose too; I've got Emacs configured
to strip that out on save.&amp;#160;&lt;a class="footnote-backref" href="#fnref:324-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/10/an-argument-with-gemini.html"/>
    <category term="AI"/>
    <category term="Coding"/>
    <category term="Gemini"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <published>2026-05-10T09:39:49+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/09/an-unreliable-buddy.html</id>
    <title>An unreliable buddy</title>
    <updated>2026-05-09T19:14:23+01:00</updated>
    <content type="html">&lt;p&gt;At some point this morning I was looking for something on this blog and
stumbled on a post that had a broken link. Not an external link, but an
internal link. This got me thinking: perhaps I should add some sort of
linting tool to &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;? I figured this
should be doable using much of the existing code: pretty much work out the
list of internal links, run through all pages and posts, see what links get
generated, look for internal links&lt;sup id="fnref:299-1"&gt;&lt;a class="footnote-ref" href="#fn:299-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, and see if they're all amongst those
that are expected.&lt;/p&gt;
&lt;p&gt;Later on in the day &lt;a href="https://github.com/davep/blogmore/issues/465" rel="noopener noreferrer" target="_blank"&gt;I prompted Copilot to have a
go&lt;/a&gt;. Now, sure, I didn't tell
it &lt;em&gt;how&lt;/em&gt; to do it, instead I told it what I wanted it to achieve. I hoped it
would (going via Claude, as I've normally let it) decide on what I felt was
the most sensible solution (use the existing configuration-reading,
page/post-finding and post-parsing code) and run with that.&lt;/p&gt;
&lt;p&gt;It didn't.&lt;/p&gt;
&lt;p&gt;Once again, as I've seen before, it seemed to understand and take into
account the existing codebase &lt;em&gt;and then copy bits from it and drop it in a
new file&lt;/em&gt;. Worse, rather than tackle this using the relevant parts of the
existing build engine, it concocted a whole new approach, again obsessing
over throwing a regex or three at the problem.&lt;/p&gt;
&lt;p&gt;I then spent the next 90 minutes or so, testing the results, finding false
reports, finding things it missed, and telling it what I found and getting
it to fix them. It did, but on occasion it seemed to special-case the fix
rather than understand the general case of what was going on and address
that.&lt;/p&gt;
&lt;p&gt;Eventually, probably too late really, I gave up trying to nudge it in the
right direction and, instead, decided it was time to be more explicit about
how it should handle this&lt;sup id="fnref:299-2"&gt;&lt;a class="footnote-ref" href="#fn:299-2"&gt;2&lt;/a&gt;&lt;/sup&gt;. The first thing that bothered me was that it
seemed to ignore the configuration object. Where BlogMore has a method of
loading the configuration into an object, which can be passed around the
code, but with the linter it loaded it up, pulled it all apart, and then
passed some of the values as a huge parameter list. Because... reasons?&lt;/p&gt;
&lt;p&gt;Anyway, I told it to cut that shit out and prompted it about a few other
things that looked pretty bad too. Copilot/Claude went off and worked away
on this for a while, using up my 6th premium request of the session, and
then eventually came back with an error telling me I'd hit a &lt;a href="https://docs.github.com/copilot/concepts/rate-limits" rel="noopener noreferrer" target="_blank"&gt;rate
limit&lt;/a&gt; and to come
back in a few hours.&lt;/p&gt;
&lt;p&gt;&lt;img alt="GitHub rate limit" height="452" loading="lazy" src="https://blog.davep.org/attachments/2026/05/09/rate-limit.webp#centre" width="988" /&gt;&lt;/p&gt;
&lt;p&gt;Could I have got it to where I wanted to be a bit earlier, with more careful
prompting? No doubt. Will a lot of people? I suspect that's rather unlikely.
This is one of the many things that make me pretty sceptical about this as
the tool some sell it as, at least for the moment. I see often that it's
written about or talked about as if it's a really useful coding buddy. It
can be, at times, but it's hugely unreliable. Here I'm testing it by
building something as a hobby, and I'm doing so knowing that there's no real
consequence if it craps out on me. I'm also doing it safe in the knowledge
that I could write the code myself, albeit at a far slower pace and with
less available time. Not everyone this is aimed at has that going for them.&lt;/p&gt;
&lt;p&gt;But these tools are still sold like they're the most reliable coding buddies
going.&lt;/p&gt;
&lt;p&gt;All that said: having hit the rate limit, and having squandered six premium
requests on the problem with no real progress, I decided to use my Google
Gemini coding allowance instead (which, in my experience so far, seems
pretty generous). I threw more or less the same initial prompt at it, but
this time I stressed that I really wanted it to use the existing engine
where possible. It managed to pretty much one-shot the problem in about 9
minutes and used up just 2% of &lt;a href="https://geminicli.com/docs/resources/quota-and-pricing/" rel="noopener noreferrer" target="_blank"&gt;my daily
quota&lt;/a&gt;&lt;sup id="fnref:299-3"&gt;&lt;a class="footnote-ref" href="#fn:299-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I've done a little more tidying up since, and I still need to properly
review &lt;a href="https://github.com/davep/blogmore/pull/467" rel="noopener noreferrer" target="_blank"&gt;the result&lt;/a&gt;, but from
what I can see of the initial results it's found all of the issues I wanted
it to find, first time (something Claude didn't manage) and hasn't found any
issues that don't exist (also something Claude didn't manage).&lt;/p&gt;
&lt;p&gt;So I guess this time Gemini was the reliable buddy. But not knowing which
buddy you can rely on makes for a pretty unreliable group of buddies.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:299-1"&gt;
&lt;p&gt;This process could, of course, work for external links too, but I'm
not really too keen on having a tool that visits every single external
link to see if it's still there.&amp;#160;&lt;a class="footnote-backref" href="#fnref:299-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:299-2"&gt;
&lt;p&gt;Which is mostly fine; I'm doing this as an experiment in what it's
capable of, and also I was sofa-hacking while having a conversation
about naming Easter eggs in Minecraft.&amp;#160;&lt;a class="footnote-backref" href="#fnref:299-2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:299-3"&gt;
&lt;p&gt;Imagine that too! Imagine knowing exactly how much of your quota
you've used at any given moment! Presumably GitHub don't show you where
you are in respect to the rate limits on top of your monthly quota
because grinding to a halt with no warning is more... fun?&amp;#160;&lt;a class="footnote-backref" href="#fnref:299-3" title="Jump back to footnote 3 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/09/an-unreliable-buddy.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <published>2026-05-09T19:14:23+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/05/me-vs-claude-redux.html</id>
    <title>Me vs Claude (redux)</title>
    <updated>2026-05-05T20:30:24+01:00</updated>
    <content type="html">&lt;p&gt;It's a small thing, but here's round 2 of &lt;a href="https://blog.davep.org/2026/05/02/me-vs-claude.html"&gt;me vs
Claude&lt;/a&gt;. This time I'm directing the agent to
clean up the code that does word counts, getting it to use the Markdown to
plain text code that exists in &lt;a href="https://blog.davep.org/tag/blogmore/"&gt;BlogMore&lt;/a&gt;, rather than the
regex-based Markdown-stripper it was using. The approach it landed on made
sense to me, adding another text extractor class, but one that ignores
fenced codeblocks&lt;sup id="fnref:319-1"&gt;&lt;a class="footnote-ref" href="#fn:319-1"&gt;1&lt;/a&gt;&lt;/sup&gt;. So, in addition to this code (I've removed all
docstrings and comments for the sake of including here):&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_AllTextExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;convert_charrefs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\s+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;it also added this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_TextWithoutCodeExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;convert_charrefs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;pre&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;pre&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\s+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The function that converts Markdown to plain text then decides which
extractor to use, based on if the caller asked for codeblocks to be included
or not.&lt;/p&gt;
&lt;p&gt;All pretty reasonable.&lt;/p&gt;
&lt;p&gt;Only... that &lt;code&gt;text&lt;/code&gt; property on both those classes is identical. The
&lt;code&gt;__init__&lt;/code&gt; method is the same save for one extra line. Even &lt;code&gt;handle_data&lt;/code&gt; is
more or less the same except for that guarding &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I can't. I can't let that stand. It's almost copy/paste. For me, this is the
ideal time to use just a little bit of inheritance. Here's my take (with
classes renamed too, the leading &lt;code&gt;_&lt;/code&gt; didn't feel necessary for one thing):&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TextExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;convert_charrefs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\s+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TextSansCodeExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextExtractor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;pre&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;pre&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pre_depth&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Much better!&lt;/p&gt;
&lt;p&gt;I was tempted to prompt Copilot/Claude about this and see what clean-up it
would do, if it would arrive at similar code. But really it didn't seem like
a good use of a premium request (perhaps I should have given Gemini a shot).&lt;/p&gt;
&lt;p&gt;I see this kind of thing in the code quite a bit, and it speaks to what I've
said before about what I'm seeing: the code it writes is... fine. It's okay.
It does the job. The code runs. It's just not... to my taste, I guess.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:319-1"&gt;
&lt;p&gt;This is important for working out word counts and so read times. It
doesn't make sense that embedded code counts towards those.&amp;#160;&lt;a class="footnote-backref" href="#fnref:319-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/05/me-vs-claude-redux.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="GitHub"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <published>2026-05-05T20:30:24+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/05/and-then-there-were-three.html</id>
    <title>And then there were three</title>
    <updated>2026-05-05T12:37:53+01:00</updated>
    <content type="html">&lt;p&gt;Given the concerns &lt;a href="https://blog.davep.org/2026/05/04/i-wouldnt-start-from-here.html"&gt;I wrote about
yesterday&lt;/a&gt;, in regard to the
core generation code in &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt;, I've been
thinking some more about how I &lt;em&gt;would&lt;/em&gt; probably have the code look. First
thing this morning, over breakfast and coffee, I concluded that I'd probably
have gone with something that was a single orchestration function/method,
into which would be composed some modular support code. Back when I &lt;a href="https://blog.davep.org/2026/05/03/a-stroppy-agent.html"&gt;started
the process of breaking up the generator&lt;/a&gt;
I seem to recall that Gemini &lt;em&gt;sort of&lt;/em&gt; went along those lines, but the code
it created seemed pretty messy and the main site generation class was still
a lot bigger than I would have liked. This is why, at the time, I went with
Copilot/Claude's mixin-based approach; it felt a bit more hacky but the code
felt tidier.&lt;/p&gt;
&lt;p&gt;With this all in mind, I popped to my desk, made a branch off &lt;a href="https://github.com/davep/blogmore/pull/452" rel="noopener noreferrer" target="_blank"&gt;the current
Gemini attempt to clean up the typing issues with the mixin
approach&lt;/a&gt;, fired up Gemini CLI,
and wrote it a prompt explaining what I didn't like and what I wanted it to
do. The key points being:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted a similar separation of concerns as the mixin approach was aiming
  for.&lt;/li&gt;
&lt;li&gt;I wanted to move away from mixins.&lt;/li&gt;
&lt;li&gt;I wanted to favour something closer to composition.&lt;/li&gt;
&lt;li&gt;I wanted to favour simple functions over classes where possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I then set it off working and left it to get on with things. Overall I think
it took around an hour, with the need for me to approve things now and again
(so probably could have been faster, I wasn't there to answer right away
every time), but it got there in the end. This has &lt;a href="https://github.com/davep/blogmore/pull/453" rel="noopener noreferrer" target="_blank"&gt;resulted in a third PR
to clean up the generator typing
issues&lt;/a&gt;. In doing so I feel I've
also addressed most of the unease I was feeling yesterday evening, and might
actually have got closer to where I'd rather the code was.&lt;/p&gt;
&lt;p&gt;Glancing over the result, I can still see things I'd want cleaned up, and
done in a slightly different way, but overall I have a better feeling about
this third approach. I sense this is a better place to move on from.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Three PRs" height="272" loading="lazy" src="https://blog.davep.org/attachments/2026/05/05/three-prs.webp#centre" width="628" /&gt;&lt;/p&gt;
&lt;p&gt;So that's three PRs I have lined up to address &lt;a href="https://github.com/davep/blogmore/issues/447" rel="noopener noreferrer" target="_blank"&gt;the code smell that's been
bugging me for a couple of
days&lt;/a&gt;. One fixes it with an
ABC; one fixes it with a protocol; and now one fixes it by reworking the
submodularisation of the generator to use a different approach entirely. On
the one hand, this seems like a lot of work and a lot of faff (and, as I
said yesterday, I wouldn't start here to get where I want to be), but on the
other hand I do kind of understand the appeal of being able to get hours of
work done in a relatively short period of time, so you can experiment with
the results.&lt;/p&gt;
&lt;p&gt;Would I recommend someone work this way? No, of course not. Does it make for
an interesting side-quest when I'm in &lt;em&gt;"it is still my hobby too"&lt;/em&gt; mode?
Yeah, it does.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/05/and-then-there-were-three.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <published>2026-05-05T12:37:53+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/04/i-wouldnt-start-from-here.html</id>
    <title>I wouldn't start from here</title>
    <updated>2026-05-04T20:59:18+01:00</updated>
    <content type="html">&lt;p&gt;The tidying of the &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt; source carries
on; sometimes by hand, but also sometimes by using either Copilot/Claude or
Gemini to decide how best to nudge the codebase in a desired direction. When
I do the latter, if I like the suggestions the agents make, but it looks
like a bunch of work and I can't be faffed with all that typing, I get them
to do the work; otherwise, I'll do it myself.&lt;/p&gt;
&lt;p&gt;I am, however, seeing lots of evidence of what I expected to happen, and
anticipated happening: to get to where I would like the code to be, I
wouldn't have started here.&lt;/p&gt;
&lt;p&gt;I'll stress again, for anyone who hasn't been following along, for anyone
who might have landed into the middle of this &lt;a href="https://blog.davep.org/category/ai/"&gt;long thread of AI
experimenting&lt;/a&gt;, that this was the point and purpose. I wanted
to use this tool to build something relatively inconsequential, and which I
could likely build myself given the time and the inclination, and also
something I would actively use.&lt;/p&gt;
&lt;p&gt;So where am I at? My main distaste at the moment is the core generation
code. Just a few days ago this was a couple of thousand lines of repetitive
code that did the job, but which was a bit messy. There's no question that I
would not have written it anything like this. Because of this I've been on a
push to try and break it up and tidy it up. While doing this I've been
playing Copilot/Claude and Gemini off against each other, to see who does
what.&lt;/p&gt;
&lt;p&gt;As of the time of writing, the generator is split up, but in a way I
wouldn't have done myself either. It's pretty much half a dozen mixin
classes in a trench coat, all pretending to be one cohesive class. I &lt;em&gt;feel&lt;/em&gt;
that's a reasonable solution given where I started, but honestly I wouldn't
have started there had I been coding this by hand.&lt;/p&gt;
&lt;p&gt;Right at the moment I'm working out the best way forward to tidy up an
outcome of this approach that I really don't like. The generator code is
littered with lots of &lt;code&gt;# type: ignore[attr-defined]&lt;/code&gt; to keep &lt;code&gt;mypy&lt;/code&gt; happy,
because that's what Claude did when it built all those little mixins. To
borrow from the explanation in &lt;code&gt;AGENTS.md&lt;/code&gt;, the current makeup looks like
this:&lt;/p&gt;
&lt;div class="highlight" data-lang="text"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;MinifyMixin
  └── AssetsMixin          (adds icons, file copying)

DateArchivesMixin
  └── ListingMixin         (adds tag/category listings)

OptionalPagesMixin
  └── PagesMixin           (adds core post/page/index/archive)

SiteGenerator(
    AssetsMixin, ContextMixin, GroupingMixin,
    ListingMixin, PagesMixin, PathsMixin
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The issue is (for example) that &lt;code&gt;MinifyMixin&lt;/code&gt; defines a method
&lt;code&gt;_write_html&lt;/code&gt;. Meanwhile &lt;code&gt;OptionalPagesMixin&lt;/code&gt; and &lt;code&gt;ListingMixin&lt;/code&gt; and so on
make use of &lt;code&gt;self._write_html&lt;/code&gt;. But because there's no direct connection
between those two classes and &lt;code&gt;MinifyMixin&lt;/code&gt;, &lt;code&gt;mypy&lt;/code&gt; complains that
&lt;code&gt;_write_html&lt;/code&gt; isn't defined. Of course, it &lt;em&gt;isn't&lt;/em&gt; defined, because it only
becomes available when all those classes climb into the &lt;code&gt;SiteGenerator&lt;/code&gt;
trench coat and pretend to be a real class.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ignore&lt;/code&gt; direction solves the problem, but it's ugly and it's cheating.&lt;/p&gt;
&lt;p&gt;So I then set the two different agents on the path of proposing a solution
to this. Both were quite different. Claude (via Copilot) decided that an
abstract base class was the solution. Gemini decided that a protocol was the
solution. I &lt;em&gt;think&lt;/em&gt; I'm siding with Gemini on this one because this is a
provides/needs problem, not a "kind of" problem. Even then though, while I
sense Gemini has the right approach, I'm not always happy with its
implementation of it&lt;sup id="fnref:338-1"&gt;&lt;a class="footnote-ref" href="#fn:338-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, and once again: it's a cleanup of something I'd
sooner not be cleaning up in the first place.&lt;/p&gt;
&lt;p&gt;So here's the thing, and this harks back to &lt;a href="https://blog.davep.org/2026/04/26/but-is-the-code-that-bad.html"&gt;wondering if the code is that
bad&lt;/a&gt;: it isn't... but it's also
generating work &lt;em&gt;if&lt;/em&gt; you look at the code and decide that you want it clean
and maintainable.&lt;/p&gt;
&lt;p&gt;To get to where I want to go, I wouldn't start from here.&lt;/p&gt;
&lt;p&gt;I get why I'm seeing the odd report here and there of people abandoning
their code bases, or deciding to rebuild them from scratch by hand. Part of
me wants to start a fresh branch, remove almost everything, and rewrite the
code so it has feature-parity but in a way where I feel the code is tidy and
elegant.&lt;/p&gt;
&lt;p&gt;The experiment is working as planned.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:338-1"&gt;
&lt;p&gt;And it feels so slow. SO. SLOW!&amp;#160;&lt;a class="footnote-backref" href="#fnref:338-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/04/i-wouldnt-start-from-here.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <published>2026-05-04T20:59:18+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/03/a-stroppy-agent.html</id>
    <title>A stroppy agent</title>
    <updated>2026-05-03T09:59:45+01:00</updated>
    <content type="html">&lt;p&gt;One of the things I noticed when I started on the &lt;a href="https://blog.davep.org/tag/blogmore/"&gt;BlogMore
experiment&lt;/a&gt; was the fact that Copilot/Claude seemed to love
to write monolithic code. Pretty early on most of the code was landing in
just a couple of files. Once I noticed this I instructed it to break things
up and always try and be more modular. This started out in the &lt;a href="https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions" rel="noopener noreferrer" target="_blank"&gt;instructions
for
Copilot&lt;/a&gt;
but eventually I migrated the instruction to &lt;code&gt;AGENTS.md&lt;/code&gt; (as seems to be the
fashion these days).&lt;/p&gt;
&lt;p&gt;While this rule seems to have held, one file that always remained pretty
large was &lt;code&gt;generator.py&lt;/code&gt;. This is, as you might guess from the name, the
main site generation code. While it does sort of make sense that it is the
pivotal body of code for the application, it doesn't follow that it &lt;em&gt;has&lt;/em&gt; to
contain so much code.&lt;/p&gt;
&lt;p&gt;So, yesterday evening, I decided to experiment by asking &lt;a href="https://blog.davep.org/tag/gemini/"&gt;Gemini
CLI&lt;/a&gt; to look over the code and tell me what it thinks. The
prompt was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Quite a bit of work has been done on @src/blogmore/generator.py to try and
reduce duplication of effort and boilerplate. I wonder if we can do a
little more? Please take a look over the code there and see if there is
any more repetitive code that can be cleaned up, to make the codebase more
maintainable.&lt;/p&gt;
&lt;p&gt;Also, the file is getting quite long. I prefer Python files to be no more
than 1000 lines at most. Please also look at the code with a view to it
being broken up into more logical sub-modules. Perhaps
@src/blogmore/generator.py could turn into a &lt;code&gt;generator&lt;/code&gt; directory with
smaller modules inside it.&lt;/p&gt;
&lt;p&gt;Look over this and report back with any findings. Also, don't look for
&lt;em&gt;anything&lt;/em&gt;, look for and report back changes that will make cleaner code
and will be impactful.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I then left it to come up with an assessment while I got on with other
things.&lt;/p&gt;
&lt;p&gt;Its headline finding was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; The file is currently &lt;strong&gt;2,146 lines long&lt;/strong&gt;, significantly
exceeding the 1,000-line maintainability threshold. It has become a "God
Object" that handles everything from low-level byte-copying and
regex-based minification to high-level site orchestration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The full assessment &lt;a href="https://gist.github.com/davep/7b89b5070589a74a72c6a25120588367" rel="noopener noreferrer" target="_blank"&gt;has been saved to a public
gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This seemed reasonable, so I told it to get on with the job of doing this.
I've also saved the plan it made &lt;a href="https://gist.github.com/davep/1494163d2b34206054575053b82f90df" rel="noopener noreferrer" target="_blank"&gt;as a
gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What is notable in the plan is the validation strategy at the end. While
there is a full suite of tests available, and while this &lt;em&gt;is&lt;/em&gt; documented in
&lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight" data-lang="markdown"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Run the test suite after every change: &lt;span class="sb"&gt;`make test`&lt;/span&gt;.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Any new functionality &lt;span class="gs"&gt;**must**&lt;/span&gt; have associated tests.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;If a change in behaviour makes existing tests incorrect, update those tests.
  Do not change tests purely to make them pass without a genuine reason.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Do not delete or comment out failing tests; fix the underlying code instead.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;the plan that Gemini arrived at seemed to concentrate only on
&lt;code&gt;test_generator.py&lt;/code&gt; and &lt;code&gt;test_integration.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once the work to break up &lt;code&gt;generator.py&lt;/code&gt; was done I could see it had run
those tests and it was happy with the outcome. So I did the obvious thing
and ran &lt;code&gt;make test&lt;/code&gt; and, sure enough, there was a failed test (I think there
was just the one). So I said to Gemini:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;you should have run all the tests. Did you read @AGENTS.md at all?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It replied with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Verifying Refactor Quality: Running comprehensive quality checks as
mandated by AGENTS.md.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and then got on with the work of fixing what had been broken.&lt;/p&gt;
&lt;p&gt;At this point it should be noted that another instruction I have in
&lt;code&gt;AGENTS.md&lt;/code&gt; is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use full, descriptive names for variables, functions, and classes. Do not
use abbreviations when the full word is readable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've found that Copilot/Claude has done an excellent job of sticking to this
wish. If anything I'd say at times it's got a little too carried away. But,
I'm not typing this code, I'm only reading it; making the code &lt;em&gt;very&lt;/em&gt;
readable from a symbol point of view makes a lot of sense.&lt;/p&gt;
&lt;p&gt;I swear, I can see why people sometimes fall into the trap of thinking
agents have personalities, because the next thing I see, after telling it
off for obviously not reading the rules of messing in my repo, is this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Single letter parameter names" height="548" loading="lazy" src="https://blog.davep.org/attachments/2026/05/03/single-letters.webp#centre" width="1266" /&gt;&lt;/p&gt;
&lt;p&gt;Now, to be fair, my instruction does mention variables, functions, and
classes. It doesn't explicitly say "parameters", I guess. But... come on!&lt;/p&gt;
&lt;p&gt;In all other respects though it got things fixed and I ended up with a
cleaned-up generation engine that was more modular. In review, I did find a
couple of things in its plan that I wasn't super keen on (and which I could
have pushed back on right at the planning stage, so I'd say that's on me,
not on the agent), but overall it was a workable solution.&lt;/p&gt;
&lt;p&gt;I prompted it once more to fix the things I didn't like, which it did and
did a fine job of. As part of that prompt I did say:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I'm seeing functions in there with single letter parameter names. Please
keep in mind the instruction about naming things in @AGENTS.md&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And it did do as it was told.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Some better naming" height="938" loading="lazy" src="https://blog.davep.org/attachments/2026/05/03/better-naming.webp#centre" width="1312" /&gt;&lt;/p&gt;
&lt;p&gt;As amusing as this was (really, it's so tempting to think it decided to be
stroppy after I told it to go read &lt;code&gt;AGENTS.md&lt;/code&gt;), it has left me wondering
though: just how widespread is the convention of looking for and reading the
agents file? While I get that each of the command-line tools seem to have a
preference for their self-named instructions file first, it was my
understanding that in the absence of such a file &lt;code&gt;AGENTS.md&lt;/code&gt; is looked for.&lt;/p&gt;
&lt;p&gt;During the session I'm talking about here, either Gemini CLI didn't do that,
or it did and just didn't take on board the conventions I wanted it to
follow.&lt;/p&gt;
&lt;p&gt;As for the great breakup of &lt;code&gt;generator.py&lt;/code&gt;... I grabbed the assessment and
the plan that Gemini came up with, &lt;a href="https://github.com/davep/blogmore/issues/442" rel="noopener noreferrer" target="_blank"&gt;turned it into an
issue&lt;/a&gt;, and &lt;a href="https://github.com/davep/blogmore/pull/443" rel="noopener noreferrer" target="_blank"&gt;set Copilot to
work on it too&lt;/a&gt;. Despite working
off the same prompt, as it were, it came up with a &lt;em&gt;very&lt;/em&gt; different
approach. So my next job is to decide which of the two I like most.&lt;/p&gt;
&lt;p&gt;As of the time of writing, the Gemini approach to cleaning this up results
in the main &lt;code&gt;site.py&lt;/code&gt; file inside the new &lt;code&gt;generator&lt;/code&gt; subdirectory being 996
lines; that's just under the 1,000 line limit I tend to set myself&lt;sup id="fnref:332-1"&gt;&lt;a class="footnote-ref" href="#fn:332-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, so
close enough, but not ideal. Copilot/Claude, on the other hand, is sat at
278 lines! While the idea of Gemini was to make &lt;code&gt;site.py&lt;/code&gt; a small
descriptive top-to-bottom and start-to-finish description of &lt;em&gt;how&lt;/em&gt; a site is
generated, it's somehow managed to make a more verbose version; the
Copilot/Claude version looks to do a far better job of fulfilling that
intention.&lt;/p&gt;
&lt;p&gt;Then again the Gemini version has broken the work up across 9 files, the
Copilot/Claude version across 13. Also the Copilot/Claude version has taken
a really fun and interesting approach to solving the problem that I'm kind
of digging&lt;sup id="fnref:332-2"&gt;&lt;a class="footnote-ref" href="#fn:332-2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So now I have to decide which, if either, I'm going with.&lt;/p&gt;
&lt;p&gt;That's probably another post.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:332-1"&gt;
&lt;p&gt;Although in my own projects I try and keep Python files much smaller
than that if I can help it.&amp;#160;&lt;a class="footnote-backref" href="#fnref:332-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:332-2"&gt;
&lt;p&gt;Spoiler: mixins. ALL THE MIXINS!&amp;#160;&lt;a class="footnote-backref" href="#fnref:332-2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/03/a-stroppy-agent.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <published>2026-05-03T09:59:45+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/02/me-vs-claude.html</id>
    <title>Me vs Claude</title>
    <updated>2026-05-02T18:48:30+01:00</updated>
    <content type="html">&lt;p&gt;After writing the &lt;a href="https://blog.davep.org/2026/05/02/it-was-such-a-simple-request.html"&gt;earlier
post&lt;/a&gt; I had to AFK to attend
to normal life things. When I finally sat back at my keyboard, I decided to
write my own take on &lt;code&gt;minified_filename&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To recap, this is what Copilot/Claude came up with first:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Compute the minified output filename for a given source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Transforms the file extension: ``.css`` becomes ``.min.css`` and&lt;/span&gt;
&lt;span class="sd"&gt;    ``.js`` becomes ``.min.js``.  For example, ``theme.js`` becomes&lt;/span&gt;
&lt;span class="sd"&gt;    ``theme.min.js`` and ``style.css`` becomes ``style.min.css``.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        source: Source filename ending in ``.css`` or ``.js``.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        The corresponding minified filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Raises:&lt;/span&gt;
&lt;span class="sd"&gt;        ValueError: If *source* does not end with ``.css`` or ``.js``.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.css&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.js&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension for minification: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is what it arrived at once it had self-reviewed the above:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Compute the minified output filename for a given source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Transforms the file extension: ``.css`` becomes ``.min.css`` and&lt;/span&gt;
&lt;span class="sd"&gt;    ``.js`` becomes ``.min.js``.  For example, ``theme.js`` becomes&lt;/span&gt;
&lt;span class="sd"&gt;    ``theme.min.js`` and ``style.css`` becomes ``style.min.css``.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        source: Source filename ending in ``.css`` or ``.js``.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        The corresponding minified filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Raises:&lt;/span&gt;
&lt;span class="sd"&gt;        ValueError: If *source* does not end with ``.css`` or ``.js``.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.css&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.js&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension for minification: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The tests it wrote looked like this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TestMinifiedFilename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test the minified_filename utility function.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_css_extension_becomes_min_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a .css extension is replaced with .min.css.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;style.min.css&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_js_extension_becomes_min_js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a .js extension is replaced with .min.js.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;theme.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;theme.min.js&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_hyphenated_css_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a hyphenated CSS filename is handled correctly.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tag-cloud.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tag-cloud.min.css&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_hyphenated_js_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a hyphenated JS filename is handled correctly.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;search.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.min.js&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_unsupported_extension_raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that an unsupported extension raises ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I wasn't too keen on the obsession with just &lt;code&gt;.css&lt;/code&gt; and &lt;code&gt;.js&lt;/code&gt; files (it
seemed unnecessary), and neither did I like the hard-coding of the resulting
extensions, etc. It all felt too job-specific.&lt;/p&gt;
&lt;p&gt;So my take on the code was this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Compute the minified output filename for a given source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        source: Source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        The corresponding minified filename.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.min&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The tests being this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TestMinifiedFilename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test the minified_filename utility function.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;before,after&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;style.min.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;theme.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;theme.min.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.min.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;style.min.min.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.file.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.file.min.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_min_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that converting a filename to the minified version has the expected effect.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So, yes, my version does work ever so slightly differently, but I feel it's
more generic. It shouldn't be the business of this function to decide which
type of file can have a &lt;code&gt;.min&lt;/code&gt; slapped prior to its extension; if a caller
asks for it, let them have it, they know what they're doing! Also, although
it's not really necessary (because the code calling on it doesn't currently
pass a &lt;code&gt;Path&lt;/code&gt;), it will accept either a &lt;code&gt;str&lt;/code&gt; or a &lt;code&gt;Path&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I feel the big difference here too is the testing. Rather than one method
after another, testing more or less the same thing with little variation, it
makes more sense to have just the one test and then pass it lots of
different input/output values. This is far more maintainable and also easier
to write most of the time.&lt;/p&gt;
&lt;p&gt;Of course, for an agent, it's probably easier for it to take a copy/paste
approach than it is for it to "reason" about what makes for a maintainable
test. I sense this is one of the dangers of letting an LLM do this job (and
it's one that's often touted as being a prime job to do): good tests can be
useful documentation if you're trying to understand a codebase.
Badly-written tests, no matter how much coverage they offer, are going to
slow you down.&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/05/02/me-vs-claude.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="GitHub"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <category term="testing"/>
    <published>2026-05-02T18:48:30+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/02/it-was-such-a-simple-request.html</id>
    <title>It was such a simple request</title>
    <updated>2026-05-02T11:26:34+01:00</updated>
    <content type="html">&lt;p&gt;As mentioned a couple of times in the last couple of days, &lt;a href="https://github.com/davep/blogmore/pull/432" rel="noopener noreferrer" target="_blank"&gt;aside from one
particular issue I found and
fixed&lt;/a&gt;, I'm in more of a &lt;em&gt;"let's
review some of the code and tidy things up"&lt;/em&gt; phase with the codebase. This
process is at times me hand-making changes, and also in part me directing
the agent to make a very specific improvement that I want.&lt;/p&gt;
&lt;p&gt;Yesterday evening I did a little experiment of getting Gemini CLI to look
for code that really needed some cleaning up, and then I had it write the
issue text &lt;a href="https://github.com/davep/blogmore/issues/434" rel="noopener noreferrer" target="_blank"&gt;which I fed directly to
Copilot/Claude&lt;/a&gt; and had it do
the work. Finally, when that was done, I had Gemini review the work that
Copilot had done (it was "happy" with &lt;a href="https://github.com/davep/blogmore/pull/435" rel="noopener noreferrer" target="_blank"&gt;the
changes&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;So, this morning, I thought I'd tackle another little thing I'd noticed in
the code that rubbed me up the wrong way. Early on in the development
lifecycle of &lt;a href="https://blogmore.davep.dev/" rel="noopener noreferrer" target="_blank"&gt;BlogMore&lt;/a&gt; I added the optional
minification of CSS and JS files (HTML too eventually, but that's not
involved here). Because it's often been a convention I also prompted Copilot
to ensure that if a file called &lt;code&gt;whatever.css&lt;/code&gt; was minified, it be called
&lt;code&gt;whatever.min.css&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The resulting code did something that made sense, but which I wouldn't ever
have done. The constants that held the filenames looked like this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;style.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;styles.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;STATS_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;stats.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;STATS_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;stats.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ARCHIVE_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;archive.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ARCHIVE_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;archive.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CALENDAR_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;calendar.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CALENDAR_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;calendar.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;GRAPH_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;graph.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;GRAPH_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;graph.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;TAG_CLOUD_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tag-cloud.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;TAG_CLOUD_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tag-cloud.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;GRAPH_JS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;graph.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;GRAPH_JS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;graph.min.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CODE_CSS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;code.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CODE_CSS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;code.min.css&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;THEME_JS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;theme.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;THEME_JS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;theme.min.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_JS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_JS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.min.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CODEBLOCKS_JS_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;codeblocks.js&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;CODEBLOCKS_JS_MINIFIED_FILENAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;codeblocks.min.js&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Like... sure, 10/10 for not hard-coding these all throughout the codebase as
magic strings&lt;sup id="fnref:309-1"&gt;&lt;a class="footnote-ref" href="#fn:309-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, but this feels a little redundant. Personally I think I'd
have just mentioned the non-minified name and then I'd have a function that
generates the minified name from it. While technically, it would add the
smallest amount of runtime overhead to the code, I think the
single-source-of-truth pay-off is worth it.&lt;/p&gt;
&lt;p&gt;For a good while though I left this alone. I was having fun playing with
other things in the application, and adding all sorts of other amusing toys.
But now that I'm more into a &lt;em&gt;"how can this code be improved and what issues
does the code have"&lt;/em&gt; mode, it felt like time to tackle this.&lt;/p&gt;
&lt;p&gt;Given that a change here would touch so much of the code, and given I wasn't
massively keen on spending ages walking through all the code and making the
changes related to this, I decided to &lt;a href="https://github.com/davep/blogmore/issues/437" rel="noopener noreferrer" target="_blank"&gt;prompt Copilot to get on with
this&lt;/a&gt;. It felt like something
it couldn't get that wrong.&lt;/p&gt;
&lt;p&gt;While it didn't get it wrong, as such, it made some questionable choices
along the way. It &lt;em&gt;did&lt;/em&gt; do the main thing I would have done: make a function
to turn a filename into a minified filename. The initial version looked like
this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Compute the minified output filename for a given source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Transforms the file extension: ``.css`` becomes ``.min.css`` and&lt;/span&gt;
&lt;span class="sd"&gt;    ``.js`` becomes ``.min.js``.  For example, ``theme.js`` becomes&lt;/span&gt;
&lt;span class="sd"&gt;    ``theme.min.js`` and ``style.css`` becomes ``style.min.css``.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        source: Source filename ending in ``.css`` or ``.js``.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        The corresponding minified filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Raises:&lt;/span&gt;
&lt;span class="sd"&gt;        ValueError: If *source* does not end with ``.css`` or ``.js``.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.css&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.js&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension for minification: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That string-slicing with &lt;code&gt;len&lt;/code&gt; and so on is nails on a chalkboard to me.
When something like &lt;code&gt;removesuffix&lt;/code&gt; exists, why on earth would "you" elect to
do this? Of course the answer is obvious, but still... ugh.&lt;/p&gt;
&lt;p&gt;Now, I will have to give credit to the process though. So the above was the
initial version of the code. Once the PR had been created by Copilot, and
I'd pulled it down for review and testing, it kicked off a review of its
own. Reviewing its own code, it pushed back on itself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In &lt;code&gt;src/blogmore/generator.py&lt;/code&gt;, lines 90-93: The slice syntax &lt;code&gt;source[:
-len(\".css\")]&lt;/code&gt; is less readable than using
&lt;code&gt;source.removesuffix(\".css\")&lt;/code&gt;, which is available in Python 3.9+. Since
this codebase targets Python 3.12+, consider using &lt;code&gt;removesuffix()&lt;/code&gt; for
clarity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It then went on to do &lt;a href="https://github.com/davep/blogmore/pull/438/changes/c11b5068833bd5b714df56291d9702359c5f9bda..b93a00f65ebf0df0597d79055f89ea4fe27d486b" rel="noopener noreferrer" target="_blank"&gt;a further
commit&lt;/a&gt;
to tidy this up. I approve. Bonus point to Copilot here.&lt;/p&gt;
&lt;p&gt;So now we have this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Compute the minified output filename for a given source filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Transforms the file extension: ``.css`` becomes ``.min.css`` and&lt;/span&gt;
&lt;span class="sd"&gt;    ``.js`` becomes ``.min.js``.  For example, ``theme.js`` becomes&lt;/span&gt;
&lt;span class="sd"&gt;    ``theme.min.js`` and ``style.css`` becomes ``style.min.css``.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        source: Source filename ending in ``.css`` or ``.js``.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        The corresponding minified filename.&lt;/span&gt;

&lt;span class="sd"&gt;    Raises:&lt;/span&gt;
&lt;span class="sd"&gt;        ValueError: If *source* does not end with ``.css`` or ``.js``.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.css&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.min.js&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension for minification: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At this point the code is less worse. I don't think it's great, but it's
less worse. Honestly, I think I'd be more inclined to do something with
&lt;a href="https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffixes" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;PurePath.suffixes&lt;/code&gt;&lt;/a&gt;
and
&lt;a href="https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;PurePath.suffix&lt;/code&gt;&lt;/a&gt;,
leaning into the fact that we're dealing with filenames here, and so making
it less about pure string slicing.&lt;/p&gt;
&lt;p&gt;I also have other issues with the code, which I might still fix by hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The fact that it makes a point of only handling &lt;code&gt;.css&lt;/code&gt; and &lt;code&gt;.js&lt;/code&gt; files,
  and throws an error otherwise, is an odd choice. I mean, in context,
  that's what it's here to serve, but it seems oddly-specific and an
  attention to detail that wasn't really necessary.&lt;/li&gt;
&lt;li&gt;The hard-coding of &lt;code&gt;.min&lt;/code&gt; a couple of times grates a little.&lt;/li&gt;
&lt;li&gt;The hard-coding of both &lt;code&gt;.css&lt;/code&gt; and &lt;code&gt;.js&lt;/code&gt; a couple of times, with the
  doubled-up &lt;code&gt;if&lt;/code&gt; feels unnecessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's a small function. It works in context. It does the job. But it also
could be more elegant in the way it does it.&lt;/p&gt;
&lt;p&gt;I'd also like to go on a small aside for a moment, because there's something
else in the above that bothers me: yesterday evening I spent some time
directing Copilot to &lt;a href="https://github.com/davep/blogmore/pull/433" rel="noopener noreferrer" target="_blank"&gt;tidy up all the docstrings in the
code&lt;/a&gt;. While any agent I've
thrown at it does seem to have taken note of &lt;a href="https://github.com/davep/blogmore/blob/53af6da034d7f42708b7cf722910d98386b648bf/AGENTS.md" rel="noopener noreferrer" target="_blank"&gt;the &lt;code&gt;AGENTS.md&lt;/code&gt;
file&lt;/a&gt;,
and the instructions on how to write the docstrings (Google style please),
it seems to have decided it was aiming more at
&lt;a href="https://www.sphinx-doc.org/en/master/" rel="noopener noreferrer" target="_blank"&gt;Sphinx&lt;/a&gt; when it came to the content.
That's fine, I hadn't been explicit.&lt;/p&gt;
&lt;p&gt;So last night I made it clear that I wanted something more like I use in all
my Python code, that aims to work with
&lt;a href="https://mkdocstrings.github.io/" rel="noopener noreferrer" target="_blank"&gt;mkdocstrings&lt;/a&gt;. It should use the inline
code and cross-reference styles that are more common when using that tool. I
even made a point of telling Copilot to update &lt;code&gt;AGENTS.md&lt;/code&gt; to make it clear
that this is the preference:&lt;/p&gt;
&lt;div class="highlight" data-lang="markdown"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;All inline code and cross-references in docstrings &lt;span class="gs"&gt;**must**&lt;/span&gt; use mkdocstrings-compatible Markdown style:
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Inline code: use single backticks (\`like_this\`).
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Cross-references: use mkdocstrings reference-style Markdown links (e.g., [&lt;span class="sb"&gt;`ClassName`&lt;/span&gt;][module.ClassName] or [&lt;span class="nt"&gt;module.ClassName&lt;/span&gt;][]).
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Do &lt;span class="gs"&gt;**not**&lt;/span&gt; use Sphinx roles (e.g., :class:`ClassName`) or double-backtick code (``ClassName``).
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now go back and look at the docstring for &lt;code&gt;minified_filename&lt;/code&gt;. So much for
agents making a point of following the instructions from &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Anyway, back to the main flow here: given that I was thinking that I might
rewrite &lt;code&gt;minified_filename&lt;/code&gt; by hand so that it works "just so", I made a
point of checking that it had written tests for this; &lt;a href="https://blog.davep.org/2026/05/01/at-least-there-are-tests.html"&gt;something I couldn't
take for granted&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Again, to the credit of the agent, it had written some tests:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TestMinifiedFilename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test the minified_filename utility function.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_css_extension_becomes_min_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a .css extension is replaced with .min.css.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;style.min.css&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_js_extension_becomes_min_js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a .js extension is replaced with .min.js.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;theme.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;theme.min.js&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_hyphenated_css_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a hyphenated CSS filename is handled correctly.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tag-cloud.css&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tag-cloud.min.css&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_hyphenated_js_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that a hyphenated JS filename is handled correctly.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;search.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;search.min.js&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_unsupported_extension_raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test that an unsupported extension raises ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unsupported file extension&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;minified_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;style.txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's a start, but I think it could be done better. There's the test of the
intended outcomes, and the test of the &lt;code&gt;ValueError&lt;/code&gt; for passing something
that isn't a &lt;code&gt;.js&lt;/code&gt; or a &lt;code&gt;.css&lt;/code&gt; file. Meanwhile, that business of testing
"hyphenated" seems oddly specific for no good reason. But it's even worse:
the test for a "hyphenated" JS file &lt;strong&gt;doesn't use a hyphenated file name&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Hilarious.&lt;/p&gt;
&lt;p&gt;That's not all. What about the more obvious things like testing what happens
if you pass a filename that has no extension, or a filename that already has
two extensions, or a filename that already ends in &lt;code&gt;.min.js&lt;/code&gt;, or a filename
that has &lt;code&gt;.min.css&lt;/code&gt; somewhere in its path that isn't at the end of the name,
or an empty string, or...&lt;/p&gt;
&lt;p&gt;Also why aren't most of these tests done using
&lt;a href="https://docs.pytest.org/en/stable/how-to/parametrize.html#pytest-mark-parametrize" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;pytest.mark.parametrize&lt;/code&gt;&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;As I said a few days ago: &lt;a href="https://blog.davep.org/2026/04/26/but-is-the-code-that-bad.html"&gt;the code is mostly
fine&lt;/a&gt;. It gets the job done. I've
seen worse. I reviewed worse. I've inherited worse. I think the thing that
concerns me the most is that there &lt;em&gt;has&lt;/em&gt; to be a lot of code like this being
uncritically accepted after generation&lt;sup id="fnref:309-2"&gt;&lt;a class="footnote-ref" href="#fn:309-2"&gt;2&lt;/a&gt;&lt;/sup&gt;, which in turn is surely &lt;a href="https://github.blog/changelog/2026-03-25-updates-to-our-privacy-statement-and-terms-of-service-how-we-use-your-data/" rel="noopener noreferrer" target="_blank"&gt;going
to be feeding back into future
training&lt;/a&gt;.
So while I can't deny that something has improved in the last six or so
months, when it comes to agent-generated code, might it be that &lt;a href="https://en.wikipedia.org/wiki/Model_collapse" rel="noopener noreferrer" target="_blank"&gt;we are at
peak quality right now&lt;/a&gt;? Might
it be that from this point on we start to decline as &lt;em&gt;"eh, it's... fine"&lt;/em&gt;
code &lt;a href="https://nitter.net/kdaigle/status/2040164759836778878" rel="noopener noreferrer" target="_blank"&gt;starts to
overwhelm&lt;/a&gt; the most
popular forge we have?&lt;/p&gt;
&lt;p&gt;&lt;img alt="This is fine" height="491" loading="lazy" src="https://blog.davep.org/attachments/2026/05/02/github-surge.webp#centre" width="512" /&gt;&lt;/p&gt;
&lt;p&gt;I suppose the main benefit still is that this approach is nice and cheap.
&lt;a href="https://fortune.com/2026/04/28/nvidia-executive-cost-of-ai-is-greater-than-cost-of-employees/" rel="noopener noreferrer" target="_blank"&gt;Right&lt;/a&gt;?&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:309-1"&gt;
&lt;p&gt;Actually, I think it &lt;em&gt;did&lt;/em&gt; hard-code the filenames throughout the
codebase, initially, until I asked it not to. Perhaps I'm
misremembering, but agents do seem to love magic strings and numbers for
some reason (I think we know the reason).&amp;#160;&lt;a class="footnote-backref" href="#fnref:309-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:309-2"&gt;
&lt;p&gt;As I have been doing with BlogMore, on purpose.&amp;#160;&lt;a class="footnote-backref" href="#fnref:309-2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/02/it-was-such-a-simple-request.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <category term="testing"/>
    <published>2026-05-02T11:26:34+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/05/01/at-least-there-are-tests.html</id>
    <title>At least there are tests</title>
    <updated>2026-05-01T17:04:26+01:00</updated>
    <content type="html">&lt;p&gt;In a post &lt;a href="https://blog.davep.org/2026/04/30/a-different-approach.html"&gt;yesterday&lt;/a&gt; I finished off
by saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At least I have, as of the time of writing, 1,380 tests to check that I've
not broken anything when I do hand-clean the code. But, hmm, there's a
question: can I &lt;em&gt;actually&lt;/em&gt; trust those tests? It's not like &lt;em&gt;I&lt;/em&gt; wrote
them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was, of course, slightly tongue-in-cheek, because I did anticipate that
the coverage might not be as useful as you'd hope an agent would deliver,
and especially not at the level you'd personally aim for. On the other hand,
I did expect it to have covered some of the fundamentals.&lt;/p&gt;
&lt;p&gt;Being serious about &lt;a href="https://blog.davep.org/2026/04/30/duplication-of-effort.html"&gt;wanting to hand-tidy some of the
code&lt;/a&gt; as a way to start to get
myself into the codebase&lt;sup id="fnref:323-1"&gt;&lt;a class="footnote-ref" href="#fn:323-1"&gt;1&lt;/a&gt;&lt;/sup&gt;, I set out to look at &lt;code&gt;validate_path_template&lt;/code&gt;
in &lt;code&gt;content_path.py&lt;/code&gt;. My plan for how to tidy the code had overlap with how
both Claude and Gemini had approached it, but also with a slightly different
take. Nothing too radical, with the main difference being that I didn't want
a baked-in default for which variables were required (to recap: both the
agents saw the need to make this configurable rather than hard-coded into
the body of the function, but both still kept a "backward-compatible"
default that had a "mixing of concerns" code smell about it).&lt;/p&gt;
&lt;p&gt;A function such as &lt;code&gt;validate_path_template&lt;/code&gt;, which has a core use, is
intended to be of fairly general utility, and which has a very obvious set
of outputs given certain inputs, and which has zero side effects and no
dependencies, seems like a really obvious candidate for a good set of unit
tests. This in turn should have meant that I could modify the code with
confidence, and experiment with confidence, knowing that said tests would
let me know when I've screwed up.&lt;/p&gt;
&lt;p&gt;I went looking for those tests so I could run them and them alone as I did
this work.&lt;/p&gt;
&lt;p&gt;Keep in mind, at this point, there are 1,380 tests that Copilot/Claude has
written. That's a lot of tests. Of course there will be some direct tests of
&lt;code&gt;validate_path_template&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Spoiler: there weren't. No tests. At all. 1,380 tests inside the &lt;code&gt;tests/&lt;/code&gt;
directory and not one that directly tested this utility function.&lt;/p&gt;
&lt;p&gt;Now, sure, the function did have coverage. Before making any changes, the
codebase itself had 94% coverage and &lt;code&gt;content_path.py&lt;/code&gt; itself had 93%
coverage. In fact, the only thing that wasn't covered was the code that
raised an exception if a template looked broken.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Coverage in main" height="232" loading="lazy" src="https://blog.davep.org/attachments/2026/05/01/validate-path-template-coverage.webp#centre" width="739" /&gt;&lt;/p&gt;
&lt;p&gt;This, for me anyway, is a good example of how and where coverage doesn't
help me. Sure, &lt;em&gt;other&lt;/em&gt; code that is being tested is calling this and if I
change this code in ways that breaks that other code, I'll (probably) get to
know about it. But if I want to properly understand the code (remember, I
didn't write it, this is like getting to know someone else's&lt;sup id="fnref:323-2"&gt;&lt;a class="footnote-ref" href="#fn:323-2"&gt;2&lt;/a&gt;&lt;/sup&gt; code) it's
really helpful to see a set of dedicated tests &lt;em&gt;for that specific function&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There were none.&lt;/p&gt;
&lt;p&gt;For a moment, I'm going to give Copilot/Claude an out. When I started
BlogMore, right at the very start, just as I was messing about to see what
would happen, I gave no thought to tests. It was only after a short while
that I asked it to a) create a set of tests for the current behaviour and b)
made it clear that all new code &lt;em&gt;had&lt;/em&gt; to have tests. It is possible, just
possible, that the content of &lt;code&gt;content_path.py&lt;/code&gt; fell through that crack. I
don't know for sure without going back and looking through the PR history.
I'm not &lt;em&gt;that&lt;/em&gt; curious right now.&lt;/p&gt;
&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; interesting though is that, in setting both Copilot/Claude and
Gemini on the same problem with the same prompt, and having them both
identify the same area for improvement, neither seemed to arrive at the
conclusion that adding dedicated tests was something worth doing.&lt;/p&gt;
&lt;p&gt;So the point here -- which isn't a revelation at all, but I think has been
nicely illustrated by what I've seen happen -- is that an agent might indeed
create a &lt;em&gt;lot&lt;/em&gt; of tests, and perhaps even achieve pretty good coverage too,
but it's no guarantee that they're going to be useful tests when you want to
get your hands dirty in the codebase.&lt;/p&gt;
&lt;p&gt;Turns out that some of those tests might still need writing by hand, like I
did for this tidy-up of &lt;code&gt;content_path.py&lt;/code&gt;. Well, I say, "by hand", I did
take this as an opportunity to &lt;a href="https://github.com/copilot-emacs/copilot.el" rel="noopener noreferrer" target="_blank"&gt;test being pretty lazy about typing out the
tests I wanted&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;PS: While looking through the tests and tidying some code related to the
above, I came across this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;blogmore.pagination_path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_PAGE_1_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_PAGE_N_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# ...other imports removed for brevity...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TestDefaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Tests for the default constant values.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_default_page_1_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;The default page_1_path should be &amp;#39;index.html&amp;#39;.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_PAGE_1_PATH&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;index.html&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_default_page_n_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;The default page_n_path should be &amp;#39;page/{page}.html&amp;#39;.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_PAGE_N_PATH&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;page/&lt;/span&gt;&lt;span class="si"&gt;{page}&lt;/span&gt;&lt;span class="s2"&gt;.html&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Brilliant. I guess &lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=YQ_xWvX1n9g" rel="noopener noreferrer" target="_blank"&gt;line goes
up&lt;/a&gt;&lt;/em&gt; has come to agent-written
tests. But look! 1,380 tests guys!&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:323-1"&gt;
&lt;p&gt;Remember: up until this point this has mostly been an experiment in
uncritically letting Copilot do its thing.&amp;#160;&lt;a class="footnote-backref" href="#fnref:323-1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:323-2"&gt;
&lt;p&gt;Arguably this &lt;em&gt;is&lt;/em&gt; someone else's code, with extra steps.&amp;#160;&lt;a class="footnote-backref" href="#fnref:323-2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content>
    <link href="https://blog.davep.org/2026/05/01/at-least-there-are-tests.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="testing"/>
    <published>2026-05-01T17:04:26+01:00</published>
  </entry>
  <entry>
    <id>https://blog.davep.org/2026/04/30/a-different-approach.html</id>
    <title>A different approach</title>
    <updated>2026-04-30T21:07:36+01:00</updated>
    <content type="html">&lt;p&gt;As mentioned in &lt;a href="https://blog.davep.org/2026/04/30/duplication-of-effort.html"&gt;the previous post&lt;/a&gt;,
I've been having a play around with Copilot/Claude vs Gemini when it comes
to getting the agents to seek out "bad" code and improve it. In that first
post on the subject, I highlighted how both tools noticed some real
duplication of effort, both addressed it in more or less the same way, and
neither of them took the clean-up to its logical conclusion (or, at the very
least, neither cleaned it up in a way that I feel is acceptable).&lt;/p&gt;
&lt;p&gt;The comparison of the two PRs
(&lt;a href="https://github.com/davep/blogmore/pull/426" rel="noopener noreferrer" target="_blank"&gt;Gemini&lt;/a&gt; vs &lt;a href="https://github.com/davep/blogmore/pull/427" rel="noopener noreferrer" target="_blank"&gt;Claude via
Copilot&lt;/a&gt;) is going to be a slow
and occasional read, and if I notice something that catches my interest,
I'll note it on this blog.&lt;/p&gt;
&lt;p&gt;Initially, I was looking at which files were touched by both. With Gemini it
was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-ca5e941617c3b9a8d18556da79f6fc1a400c37ae04ea1ab96efff195b60185fd" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;content_path.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-9bcd414441be257b448351de586a6d3c43436bc613dd542e404d8c7f080bd24a" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;generator.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-5a5ec66899ab350bb8d841c129fe67d4dde77b17a55d8d24af583a02049d8693" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;pagination_path.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-9efe9541885322ac1491d0a7c47d29a9b3dabe129227199a95c8056907b81b97" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;parser.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-29ac7d19a2c8f6766a2fd48d76cbf6cc40216eb43b9cd32d751d9c93818377f0" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;renderer.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/426/changes#diff-403ab488e41682972adf6ba09e3ed2a92de5812d8338b95112a8dd75bf0296f0" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;utils.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And with Copilot/Claude:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-e1865709216be5988af59605690d590acd5544e63f95b4df6b85f9b0d755b448" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;markdown/first_paragraph.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-fcabf04c8545cb76d4000f86e286ab4325e6a55eae20f0b9441c57129c57bb40" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;comment_invite.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-ca5e941617c3b9a8d18556da79f6fc1a400c37ae04ea1ab96efff195b60185fd" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;content_path.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-4193670647dc0d7b85a7ac81afbde4954cb2e46d0216e0e6abac33f49d2459b4" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;feeds.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-5a5ec66899ab350bb8d841c129fe67d4dde77b17a55d8d24af583a02049d8693" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;pagination_path.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-9efe9541885322ac1491d0a7c47d29a9b3dabe129227199a95c8056907b81b97" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;parser.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-2dd82dfd583162734cc70424abd703859cb283038ed1d9e4220fd50d5c60a9e0" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;post_path.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davep/blogmore/pull/427/changes#diff-403ab488e41682972adf6ba09e3ed2a92de5812d8338b95112a8dd75bf0296f0" rel="noopener noreferrer" target="_blank"&gt;&lt;code&gt;utils.py&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the surface, it looks like Claude might have done a better job of finding
untidy issues in the code. Of course a proper read/assessment of the outcome
is needed to decide which is "better"; not to mention the application of a
lot of personal taste.&lt;/p&gt;
&lt;p&gt;So, with the initial/surface impression that "Claude went deeper", I took a
look at the first file they had in common: &lt;code&gt;content_path.py&lt;/code&gt;. This is
documented as a module related to:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shared path-resolution utilities for content output paths.&lt;/p&gt;
&lt;p&gt;This module provides the generic building blocks used by &lt;code&gt;page_path&lt;/code&gt; and
&lt;code&gt;post_path&lt;/code&gt;. Each content type supplies its own allowed-variable set and
variable dict; this module handles the common validation, substitution,
and safety checks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's 3 functions in there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validate_path_template&lt;/code&gt; -- for validating a format string used in
  building a path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve_path&lt;/code&gt; -- given a template and some values to populate variables
  in the template, create a path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;safe_output_path&lt;/code&gt; -- helper function for joining paths and ensuring they
  don't escape the output directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These seem like sensible functions to have in here, and I can imagine me
writing a similar set in terms of the problem they seek to solve.&lt;/p&gt;
&lt;p&gt;Both agents seemed to agree on what needed some work:
&lt;code&gt;validate_path_template&lt;/code&gt;. Both also seem to agree that building knowledge of
which variable is required into the function itself isn't terribly flexible;
I feel this is a reasonable review of the situation. However, the two agents
seem to disagree on how this should be resolved.&lt;/p&gt;
&lt;p&gt;Claude's take on this is that the function should grow an optional keyword
argument called &lt;code&gt;required_variable&lt;/code&gt;, which defaults to &lt;code&gt;slug&lt;/code&gt;. It also adds
an &lt;code&gt;assert&lt;/code&gt; to test if the required variable exists in the
&lt;code&gt;allowed_variables&lt;/code&gt; (okay, I could quibble about this but given this is a
code-check rather than a user-input check, eh, I can go with it). Finally it
does the check using the new variable and also makes the error reporting a
touch more generic too.&lt;/p&gt;
&lt;div class="highlight" data-lang="diff"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;--- /Users/davep/content_path.py        2026-04-30 13:20:00.737955197 +0100&lt;/span&gt;
&lt;span class="gi"&gt;+++ src/blogmore/content_path.py        2026-04-30 13:20:04.560178727 +0100&lt;/span&gt;
&lt;span class="gu"&gt;@@ -17,13 +17,15 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    template: str,
&lt;span class="w"&gt; &lt;/span&gt;    config_key: str,
&lt;span class="w"&gt; &lt;/span&gt;    allowed_variables: frozenset[str],
&lt;span class="gd"&gt;-    item_name: str,&lt;/span&gt;
&lt;span class="gi"&gt;+    item_name: str = &amp;quot;&amp;quot;,&lt;/span&gt;
&lt;span class="gi"&gt;+    *,&lt;/span&gt;
&lt;span class="gi"&gt;+    required_variable: str | None = &amp;quot;slug&amp;quot;,&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;) -&amp;gt; None:
&lt;span class="w"&gt; &lt;/span&gt;    &amp;quot;&amp;quot;&amp;quot;Validate a path format string for a content type.

&lt;span class="w"&gt; &lt;/span&gt;    Checks that *template* is non-empty, well-formed, references only
&lt;span class="gd"&gt;-    variables from *allowed_variables*, and includes the mandatory&lt;/span&gt;
&lt;span class="gd"&gt;-    ``{slug}`` placeholder.&lt;/span&gt;
&lt;span class="gi"&gt;+    variables from *allowed_variables*, and (when *required_variable* is&lt;/span&gt;
&lt;span class="gi"&gt;+    not ``None``) includes the mandatory placeholder.&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;    Args:
&lt;span class="w"&gt; &lt;/span&gt;        template: The path format string to validate.
&lt;span class="gu"&gt;@@ -33,11 +35,19 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            template.
&lt;span class="w"&gt; &lt;/span&gt;        item_name: The human-readable name of the content type used in
&lt;span class="w"&gt; &lt;/span&gt;            the uniqueness error message (e.g. ``&amp;quot;page&amp;quot;`` or ``&amp;quot;post&amp;quot;``).
&lt;span class="gi"&gt;+            Ignored when *required_variable* is ``None``.&lt;/span&gt;
&lt;span class="gi"&gt;+        required_variable: The variable name that must appear in the&lt;/span&gt;
&lt;span class="gi"&gt;+            template, or ``None`` if no variable is mandatory.  Defaults&lt;/span&gt;
&lt;span class="gi"&gt;+            to ``&amp;quot;slug&amp;quot;`` for backward compatibility.&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;    Raises:
&lt;span class="w"&gt; &lt;/span&gt;        ValueError: If the template is empty, malformed, references an
&lt;span class="gd"&gt;-            unknown variable, or omits the ``{slug}`` placeholder.&lt;/span&gt;
&lt;span class="gi"&gt;+            unknown variable, or omits the required placeholder.&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    &amp;quot;&amp;quot;&amp;quot;
&lt;span class="gi"&gt;+    assert required_variable is None or required_variable in allowed_variables, (&lt;/span&gt;
&lt;span class="gi"&gt;+        f&amp;quot;required_variable {required_variable!r} is not in allowed_variables&amp;quot;&lt;/span&gt;
&lt;span class="gi"&gt;+    )&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    if not template:
&lt;span class="w"&gt; &lt;/span&gt;        raise ValueError(f&amp;quot;{config_key} must not be empty&amp;quot;)

&lt;span class="gu"&gt;@@ -61,9 +71,9 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            + f&amp;quot;. Allowed variables are: {&amp;#39;, &amp;#39;.join(sorted(allowed_variables))}&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;        )

&lt;span class="gd"&gt;-    if &amp;quot;slug&amp;quot; not in field_names:&lt;/span&gt;
&lt;span class="gi"&gt;+    if required_variable is not None and required_variable not in field_names:&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        raise ValueError(
&lt;span class="gd"&gt;-            f&amp;quot;{config_key} &amp;#39;{template}&amp;#39; must contain the {{slug}} variable so that &amp;quot;&lt;/span&gt;
&lt;span class="gi"&gt;+            f&amp;quot;{config_key} &amp;#39;{template}&amp;#39; must contain the {{{required_variable}}} variable so that &amp;quot;&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            f&amp;quot;each {item_name} can be uniquely identified&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;        )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Gemini, on the other hand, has a very similar idea but allows for the fact
that the caller might want to specify multiple required variables. So in
this case it adds &lt;code&gt;required_variables&lt;/code&gt; (as a positional/keyword argument
rather than a pure-keyword argument) and defaults it to a &lt;code&gt;frozenset&lt;/code&gt; that
contains &lt;code&gt;"slug"&lt;/code&gt;. The rest of the change is also about making the test for
the required variables, and the reporting of the error, generic. It
&lt;em&gt;doesn't&lt;/em&gt; do anything about checking that the required variables are within
the allowed variables.&lt;/p&gt;
&lt;div class="highlight" data-lang="diff"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gd"&gt;--- /Users/davep/content_path.py        2026-04-30 13:20:00.737955197 +0100&lt;/span&gt;
&lt;span class="gi"&gt;+++ src/blogmore/content_path.py        2026-04-30 14:47:41.607748447 +0100&lt;/span&gt;
&lt;span class="gu"&gt;@@ -18,12 +18,13 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    config_key: str,
&lt;span class="w"&gt; &lt;/span&gt;    allowed_variables: frozenset[str],
&lt;span class="w"&gt; &lt;/span&gt;    item_name: str,
&lt;span class="gi"&gt;+    required_variables: frozenset[str] = frozenset({&amp;quot;slug&amp;quot;}),&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;) -&amp;gt; None:
&lt;span class="w"&gt; &lt;/span&gt;    &amp;quot;&amp;quot;&amp;quot;Validate a path format string for a content type.

&lt;span class="w"&gt; &lt;/span&gt;    Checks that *template* is non-empty, well-formed, references only
&lt;span class="gd"&gt;-    variables from *allowed_variables*, and includes the mandatory&lt;/span&gt;
&lt;span class="gd"&gt;-    ``{slug}`` placeholder.&lt;/span&gt;
&lt;span class="gi"&gt;+    variables from *allowed_variables*, and includes the&lt;/span&gt;
&lt;span class="gi"&gt;+    *required_variables*.&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;    Args:
&lt;span class="w"&gt; &lt;/span&gt;        template: The path format string to validate.
&lt;span class="gu"&gt;@@ -33,10 +34,13 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            template.
&lt;span class="w"&gt; &lt;/span&gt;        item_name: The human-readable name of the content type used in
&lt;span class="w"&gt; &lt;/span&gt;            the uniqueness error message (e.g. ``&amp;quot;page&amp;quot;`` or ``&amp;quot;post&amp;quot;``).
&lt;span class="gi"&gt;+        required_variables: The set of variable names that MUST appear&lt;/span&gt;
&lt;span class="gi"&gt;+            in the template to ensure uniqueness. Defaults to&lt;/span&gt;
&lt;span class="gi"&gt;+            ``{&amp;quot;slug&amp;quot;}``.&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;    Raises:
&lt;span class="w"&gt; &lt;/span&gt;        ValueError: If the template is empty, malformed, references an
&lt;span class="gd"&gt;-            unknown variable, or omits the ``{slug}`` placeholder.&lt;/span&gt;
&lt;span class="gi"&gt;+            unknown variable, or omits a required variable.&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    &amp;quot;&amp;quot;&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;    if not template:
&lt;span class="w"&gt; &lt;/span&gt;        raise ValueError(f&amp;quot;{config_key} must not be empty&amp;quot;)
&lt;span class="gu"&gt;@@ -61,10 +65,12 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            + f&amp;quot;. Allowed variables are: {&amp;#39;, &amp;#39;.join(sorted(allowed_variables))}&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;        )

&lt;span class="gd"&gt;-    if &amp;quot;slug&amp;quot; not in field_names:&lt;/span&gt;
&lt;span class="gi"&gt;+    missing = required_variables - set(field_names)&lt;/span&gt;
&lt;span class="gi"&gt;+    if missing:&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        raise ValueError(
&lt;span class="gd"&gt;-            f&amp;quot;{config_key} &amp;#39;{template}&amp;#39; must contain the {{slug}} variable so that &amp;quot;&lt;/span&gt;
&lt;span class="gd"&gt;-            f&amp;quot;each {item_name} can be uniquely identified&amp;quot;&lt;/span&gt;
&lt;span class="gi"&gt;+            f&amp;quot;{config_key} &amp;#39;{template}&amp;#39; must contain the &amp;quot;&lt;/span&gt;
&lt;span class="gi"&gt;+            + &amp;quot;, &amp;quot;.join(f&amp;quot;{{{v}}}&amp;quot; for v in sorted(missing))&lt;/span&gt;
&lt;span class="gi"&gt;+            + f&amp;quot; variable(s) so that each {item_name} can be uniquely identified&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For the most part I think I prefer what Gemini is trying to do, although
Claude's sanity check that the required variable is one of the possible
variables makes sense. I kind of feel like both of them missed the point
when it came to handling the fact that &lt;code&gt;"slug"&lt;/code&gt; is required: given that
&lt;code&gt;validate_path&lt;/code&gt; is otherwise built to be pretty generic, I think I would
have defaulted to nothing and simply left it up to the caller to be explicit
that &lt;code&gt;"slug"&lt;/code&gt; is required, because that matters in context of the caller.
This feels like a pretty obvious case of a "business logic" vs "generic
utility code" separation of concerns scenario.&lt;/p&gt;
&lt;p&gt;As &lt;a href="https://blog.davep.org/2026/04/26/but-is-the-code-that-bad.html"&gt;mentioned in passing in another
post&lt;/a&gt;, it's interesting to see
that neither of them noticed the opportunity to turn this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field_names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed_variables&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;into this:&lt;/p&gt;
&lt;div class="highlight" data-lang="python"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field_names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed_variables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I know &lt;a href="https://darren.codes/" rel="noopener noreferrer" target="_blank"&gt;at least one person&lt;/a&gt; who would be happy about
this fact.&lt;/p&gt;
&lt;p&gt;So where does this leave me? At the moment I'm not inclined to merge either
PR, but that's mainly because I want to carry on reading them and perhaps
writing some more notes about what I encounter. What this does illustrate
for me is something we know well enough anyway, but which I wanted to
experiment with and see for myself: the initial implementation of any
working code written by an agent seems optimised for that particular
function or method, perhaps class if you're lucky. It will happily repeat
the same code to solve similar problems, or perhaps even use very different
approaches to solve the same problem. What it won't do well is recognise
that this problem is solved elsewhere and so either use that other code by
calling it, or perhaps modify it slightly to make it more generic and more
applicable in more situations.&lt;/p&gt;
&lt;p&gt;On the other hand, it &lt;em&gt;has&lt;/em&gt; shown that with a bit of prompting (and keep in
mind that the prompt that arrived at this comparison was really quite vague)
it is possible to get an agent to "consider" the problem of duplication and
boilerplate and to try and address that.&lt;/p&gt;
&lt;p&gt;Having seen the two solutions on offer here, it's hard not to conclude that
the best solution would be for me to take the PRs as flags marking places in
the code that could be cleaned up, and do the tidy myself.&lt;/p&gt;
&lt;p&gt;At least I have, as of the time of writing, 1,380 tests to check that I've
not broken anything when I do hand-clean the code. But, hmm, there's a
question: can I &lt;em&gt;actually&lt;/em&gt; trust those tests? It's not like &lt;em&gt;I&lt;/em&gt; wrote them.&lt;/p&gt;
&lt;p&gt;Guess that's a whole other thing to worry about at some point...&lt;/p&gt;</content>
    <link href="https://blog.davep.org/2026/04/30/a-different-approach.html"/>
    <category term="AI"/>
    <category term="BlogMore"/>
    <category term="Coding"/>
    <category term="Copilot"/>
    <category term="Gemini"/>
    <category term="GitHub"/>
    <category term="Google"/>
    <category term="Python"/>
    <category term="code review"/>
    <category term="code smell"/>
    <published>2026-04-30T21:07:36+01:00</published>
  </entry>
</feed>
