<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://oddlittlebird.github.io/showcase-site/blog</id>
    <title>Diana Payton Blog</title>
    <updated>2026-10-06T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://oddlittlebird.github.io/showcase-site/blog"/>
    <subtitle>Diana Payton Blog</subtitle>
    <icon>https://oddlittlebird.github.io/showcase-site/img/favicon.svg</icon>
    <entry>
        <title type="html"><![CDATA[Version pinning used to be someone else's job]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job"/>
        <updated>2026-10-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Two failed tests turned out to be a GitHub outage. The notice next to them was about something I'd never had to think about before this project.]]></summary>
        <content type="html"><![CDATA[<p>Before I started this portfolio-with-a-pipeline project, I didn't know that pinning a version was something anyone needed to do. Now, I know it's one of the things keeping my pipeline from breaking when a software version updates.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-failure-emails">Two failure emails<a href="https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job#two-failure-emails" class="hash-link" aria-label="Direct link to Two failure emails" title="Direct link to Two failure emails" translate="no">​</a></h2>
<p>I got a couple of emails the other morning saying that two of my scheduled tests had failed: the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/llms-txt-test">llms.txt verification test</a> and the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference screenshot test</a>. As one does, I asked my robot minion (Claude Code) to find out why.</p>
<p>Neither test had actually run. Every workflow in my repo runs on a temporary machine that GitHub supplies, and the message on both jobs said no machine ever picked the job up. GitHub's status page showed an Actions incident that started about 20 minutes before my tests were scheduled. The tests that ran earlier and later that day all passed. The fix was to wait for the next scheduled run. So, no big deal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-notice-next-to-the-failures">The notice next to the failures<a href="https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job#the-notice-next-to-the-failures" class="hash-link" aria-label="Direct link to The notice next to the failures" title="Direct link to The notice next to the failures" translate="no">​</a></h2>
<p>Claude Code also pointed out a notice that GitHub had attached to every run, passing or failing. Starting October 19, the <code>ubuntu-latest</code> label moves to Ubuntu 26.</p>
<p>Every one of my workflows had the line <code>runs-on: ubuntu-latest</code>. That line doesn't name a version. It asks for whatever GitHub currently calls the latest one. On October 19, all of my workflows would move to a new operating system, and I wouldn't have changed a thing in my repo.</p>
<p>Most of my pipeline probably wouldn't notice. The screenshot tests are the ones I'd worry about. They compare a new screenshot against a saved one, and a different operating system can render fonts and spacing a little differently. A test could fail over a few pixels, and I'd spend a morning chasing a problem that was never in my content. No thank you!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="id-seen-this-before">I'd seen this before<a href="https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job#id-seen-this-before" class="hash-link" aria-label="Direct link to I'd seen this before" title="Direct link to I'd seen this before" translate="no">​</a></h2>
<p>Pinning had come up before. Earlier this year, I'd pinned Doc Detective to a specific version so that an update couldn't break my tests without warning. In September, I found out the hard way that the pin didn't cover a browser driver that Doc Detective downloads on its own (grr). That one's written up in <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer">I'm the supervisor, not the engineer</a>.</p>
<p>The fix here made sense to me right away. Claude Code changed every workflow from <code>ubuntu-latest</code> to <code>ubuntu-24.04</code>, the version they were already running on. That's 16 jobs across 14 workflow files. They run exactly as before, but the upgrade now happens when I decide it does, instead of on GitHub's calendar.</p>
<p>The pin isn't permanent. GitHub will eventually retire Ubuntu 24.04, so at some point I'll try Ubuntu 26 on a separate branch, refresh any saved screenshots that come out different, and then take the pin out. In fact, I told Claude Code to remind me in November so that I can test it out after the initial flurry of fixes and release pains are past.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="somebody-used-to-do-this-for-me">Somebody used to do this for me<a href="https://oddlittlebird.github.io/showcase-site/blog/version-pinning-used-to-be-someone-elses-job#somebody-used-to-do-this-for-me" class="hash-link" aria-label="Direct link to Somebody used to do this for me" title="Direct link to Somebody used to do this for me" translate="no">​</a></h2>
<p>In my previous life, I was a technical writer at a company where an ops team handled this kind of thing. I never saw a runner label. If a build machine changed under me, then someone else whose job it was knew about it first and dealt with it. I never heard about it.</p>
<p>Owning the repo means I'm that someone now. It also means I can talk about the pros and cons of pinning a version with an engineer and actually contribute meaningfully to the conversation. It's incredibly empowering.</p>
<p>If Git and pipelines still feel like black magic to you, set up a project of your own and own it for a while. You'll run into things you never expected to learn about, and you'll be glad you did.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[I'm the supervisor, not the engineer]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer"/>
        <updated>2026-09-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A technical writer asked me whether they were technical enough to build a docs pipeline with an AI assistant. Here's what a week of fixing mine actually asked of me.]]></summary>
        <content type="html"><![CDATA[<p>I was talking with a technical writer recently who doesn't use GitHub. They'd been reading about docs-as-code pipelines and wanted to know whether they were technical enough to build one with Claude Code or a similar AI assistant.</p>
<p>It's a fair question, and I didn't want to just answer it with a pep talk. This is what happened on my own pipeline this week, including the part I'm less proud of.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-i-work-with-claude-code">How I work with Claude Code<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#how-i-work-with-claude-code" class="hash-link" aria-label="Direct link to How I work with Claude Code" title="Direct link to How I work with Claude Code" translate="no">​</a></h2>
<p>Claude Code is my assistant and pocket engineer. My account settings tell it, "I am a technical writer, not an engineer," so it explains things to me the way it would to a technical layperson. It does the investigating and the building. I decide what gets built, and when an explanation loses me, I ask for further clarification. Unlike a human engineer, Claude Code never gets frustrated or annoyed with my questions.</p>
<p>One more habit, which is my own quirk: Claude Code commits its changes to a separate branch, and sometimes pushes that branch to GitHub, but I'm always the one who does the final merge into the live site. I like having that manual review step, even when I'm confident in the change.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-red-tests">Two red tests<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#two-red-tests" class="hash-link" aria-label="Direct link to Two red tests" title="Direct link to Two red tests" translate="no">​</a></h2>
<p>This site runs a set of <a href="https://doc-detective.com/" target="_blank" rel="noopener noreferrer" class="">Doc Detective</a> tests on a schedule. Each one checks something the site claims, and each has a status badge on its page. Earlier this week, two of them started failing: the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/pipeline-test">Pipeline test</a> and the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference screenshot test</a>.</p>
<p>I saw the red in the GitHub Actions tab, pasted the link into Claude Code, and told it those two tests were failing. That was the whole request. (Sometimes I actually go into the logs and paste more details, but I didn't this time.)</p>
<p>It found three things, in this order:</p>
<ul>
<li class="">The failures weren't about my content. The pipeline page still had exactly the heading the test looks for.</li>
<li class="">It ran the tests on my laptop and got the same failure. Then it pointed a test at example.com, a page with nothing to do with my site, and that failed too. So the problem was in the testing tool.</li>
<li class="">Doc Detective drives a real browser, and to do that it downloads some helper software the first time it runs. One of those helpers, a Chrome driver, released a new version on September 14 that broke the step where a test looks for something on the page. The timing lined up almost to the minute: the pipeline test passed at 16:05 UTC, the new driver came out at 16:18, and the API reference test failed for the first time at 16:51.</li>
</ul>
<p>Here's the part that surprised me: I'd pinned Doc Detective to a specific version so an update couldn't break things without warning. It turns out that pin doesn't cover the helpers Doc Detective downloads. Those still came in at whatever the newest version was.</p>
<p>The fix was to hold that one driver at its previous version. Claude Code's first attempt at that didn't stick, because Doc Detective's own install step quietly replaced it with the new version again. Claude Code caught that in its own testing before it ever showed me the change, tried a different approach, and confirmed both tests passed. It also told me the version hold is temporary. Once a fixed driver ships, the hold should come out, or I'll miss future fixes too.</p>
<p>I merged the change and watched the Actions tab. Both tests went green.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-were-there-so-many-deploys">Why were there so many deploys?<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#why-were-there-so-many-deploys" class="hash-link" aria-label="Direct link to Why were there so many deploys?" title="Direct link to Why were there so many deploys?" translate="no">​</a></h2>
<p>Then I noticed something else in the Actions tab. My one merge had set off a stack of Deploy runs, and several had a gray warning icon. Deploy is the workflow that rebuilds the site and publishes it. I only expected one deploy per merge, so I asked what was going on.</p>
<p>The gray icons turned out to be harmless. Only one deploy can run at a time, so when several line up, GitHub cancels the older ones that are still waiting and runs the newest. The cancelled ones never used any time or resources.</p>
<p>But Claude Code also checked the deploy history and found something I hadn't known: the site was rebuilding and redeploying itself seven times a day. Most of my scheduled tests save a small status file when they finish, and each one triggered its own deploy. Nothing had broken, and because the repo is public, the extra runs didn't cost anything. It was just a lot of redundant work.</p>
<p>Claude Code gave me three options, each with its trade-off:</p>
<ul>
<li class="">Deploy once a day.</li>
<li class="">Deploy only when a test result actually changes.</li>
<li class="">Leave it alone and just fix two tests whose results weren't getting published at all.</li>
</ul>
<p>I pointed out that my <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/doodle-test">Doodle test</a> captures Google's homepage every day, so at minimum the site needs a daily deploy. Once a day covered that, so that's what I picked.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-mistake-i-caught-by-asking-a-plain-question">The mistake I caught by asking a plain question<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#the-mistake-i-caught-by-asking-a-plain-question" class="hash-link" aria-label="Direct link to The mistake I caught by asking a plain question" title="Direct link to The mistake I caught by asking a plain question" translate="no">​</a></h2>
<p>Before merging, I asked Claude Code one more thing: do any of the pipeline descriptions or test write-ups need to change because of this?</p>
<p>That question turned up a real problem in the change itself. Claude Code had scheduled the daily deploy to run after the check it described as the last one of the day. It wasn't the last one. The doodle test runs twice a day, and its second run is a fallback for days when Google hasn't put up the new doodle yet. That second run happens after the deploy would have run, so on exactly the days the fallback exists for, the doodle page would have shown yesterday's image until the next day.</p>
<p>Claude Code said so directly, said it should have checked every schedule before calling anything the last one, and laid out new options. I chose to deploy after each doodle run: two deploys a day instead of seven. The same review also found two descriptions on the site that were already wrong before this change, and I had it fix those in the same branch.</p>
<p>I never wrote a line of that code. What I did was ask whether the documentation still matched the system. As a technical writer, that's the question I'd ask anyway.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-seven-deploys-a-day-a-mistake">Was seven deploys a day a mistake?<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#was-seven-deploys-a-day-a-mistake" class="hash-link" aria-label="Direct link to Was seven deploys a day a mistake?" title="Direct link to Was seven deploys a day a mistake?" translate="no">​</a></h2>
<p>An engineer designing the whole pipeline in one sitting probably would have set up one deploy from the start, but nobody designed my pipeline in one sitting. It grew one test at a time over several months, and each test's deploy trigger made sense when I added it. When I wrote <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos">Doc Detective dominos</a>, triggering a deploy after the doodle test was the fix.</p>
<p>No one had ever looked at the deploy setup as a whole, including me.</p>
<p>The "last check of the day" error bothers me more. Claude Code stated it with complete confidence, and it would have gotten past me if I hadn't asked a follow-up question. I wrote about the same gap in <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice">What Claude Code didn't notice</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="back-to-the-technical-writers-question">Back to the technical writer's question<a href="https://oddlittlebird.github.io/showcase-site/blog/im-the-supervisor-not-the-engineer#back-to-the-technical-writers-question" class="hash-link" aria-label="Direct link to Back to the technical writer's question" title="Direct link to Back to the technical writer's question" translate="no">​</a></h2>
<p>Could they build this? I think so. Everything I did this week came down to noticing that something looked off in the Actions tab and bringing it to Claude Code, then basically having a conversation with it.</p>
<p>At the start of the week, I didn't know that Doc Detective's helpers ignore my version pin, that my site was deploying seven times a day, or how GitHub handles a queue of deploys. None of that was a prerequisite. I found out each one as it came up.</p>
<p>Now, I know more than I did at the start of the week. I'm building my expertise as I build the pipeline, iterating as I go. And that part is working exactly as designed.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The file with no metadata passed every check]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check"/>
        <updated>2026-08-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[My pipeline had three quality gates and none of them looked at front matter. The file that describes the pipeline to machines had no front matter at all, and every check passed it.]]></summary>
        <content type="html"><![CDATA[<p>There is a file in this repo called <code>pipeline.md</code>. It exists so an AI agent can read a straight description of how this site's documentation pipeline works. It's the file my whole "documentation for machines" argument rests on.</p>
<p>Until last week, it had no front matter. None. It opened on an H1. That's one of the dangers when you build something with an AI coding assistant like Claude Code, I guess.</p>
<p>Vale read it. Doc Detective read it. Docusaurus built it. Every check passed, including me physically looking at it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-a-third-gate">Adding a third gate<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#adding-a-third-gate" class="hash-link" aria-label="Direct link to Adding a third gate" title="Direct link to Adding a third gate" translate="no">​</a></h2>
<p>I had Claude Code add Manny Silva's new tool, <a href="https://hawkeyexl.github.io/docmeta/" target="_blank" rel="noopener noreferrer" class="">docmeta</a>, to the pipeline this week. The split is clean. Vale checks how the prose reads, Doc Detective checks whether the procedures still work, and docmeta checks the metadata. Before this, nothing in my stack looked at front matter at all.</p>
<p>One thing to know before you try docmeta: It needs Node 24. The Get started page says Node 20 or newer, but the package has required Node 24 since its first release, and npm reports that mismatch as a warning rather than an error. So the install says it succeeded, <code>docmeta --version</code> answers normally, and the thing quietly doesn't work. My CI workflows ran Node 22, which would have produced the same silent non-failure. Claude wrote the whole thing up as a bug report that I sent to Manny, so the Get started should be updated very soon.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-first-run-told-me-nothing">The first run told me nothing<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#the-first-run-told-me-nothing" class="hash-link" aria-label="Direct link to The first run told me nothing" title="Direct link to The first run told me nothing" translate="no">​</a></h2>
<p>docmeta ships with schemas for Docusaurus front matter, so I started with those. Thirty files across my docs, blog, and pages directories:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">30 files checked, 30 passed, 0 failed, 0 errors</span><br></div></code></pre></div></div>
<p>A clean sweep, which sounds like good news but was actually the entire problem.</p>
<p>The built-in schema is honest about this in its own description: "Docusaurus marks no field as required, so this is a format check, not a presence check." It constrains the shape of every field Docusaurus accepts, and it requires none of them. A file with no front matter whatsoever satisfies it completely, which is how <code>pipeline.md</code> passed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eleven-lines-changed-the-answer">Eleven lines changed the answer<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#eleven-lines-changed-the-answer" class="hash-link" aria-label="Direct link to Eleven lines changed the answer" title="Direct link to Eleven lines changed the answer" translate="no">​</a></h2>
<p>So I had Claude write a schema of my own. Eleven lines of properties:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"object"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"additionalProperties"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"required"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"title"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"description"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"last_reviewed"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"properties"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"title"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"string"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"minLength"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"description"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"string"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"minLength"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"last_reviewed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"string"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"format"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"date"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The first version required all three fields, which failed all 30 files and told me nothing except that I had written a strict schema. I asked for the honest number instead: what fails on title and description alone, two fields I was already using. On a site I built specifically to demonstrate documentation quality:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">24 of 30 files failed</span><br></div></code></pre></div></div>
<p>Six passed. All six live in <code>src/pages</code>. Not one file in <code>docs/</code> or <code>blog/</code> had a description. Fifteen blog posts, zero descriptions, every one of them written by someone who does this for a living.</p>
<p><code>description</code> is the field that feeds search results and AI retrieval. It had been missing from almost everything for months, and no gate I owned had any opinion about it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-date-that-is-not-a-date">The date that is not a date<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#the-date-that-is-not-a-date" class="hash-link" aria-label="Direct link to The date that is not a date" title="Direct link to The date that is not a date" translate="no">​</a></h2>
<p>Claude built a test file with a title, a description, and <code>last_reviewed: 2024-13-45</code>, which is month thirteen, day forty-five. Against the Docusaurus schema:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">1 file checked, 1 passed, 0 failed, 0 errors</span><br></div></code></pre></div></div>
<p>Against my schema:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">/last_reviewed  must match format "date"  (line 4)</span><br></div></code></pre></div></div>
<p>That gap is the whole argument. If I ever build a staleness report on <code>last_reviewed</code>, the report doesn't error when the field is garbage. It just returns a wrong answer, and nothing in the output marks it as one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-things-i-got-wrong">Two things I got wrong<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#two-things-i-got-wrong" class="hash-link" aria-label="Direct link to Two things I got wrong" title="Direct link to Two things I got wrong" translate="no">​</a></h2>
<p><strong>The check that would never have run.</strong> Claude wired docmeta into my existing lint workflow, and the plan was to push the check on its own first, so I could screenshot it failing before the fix landed. It caught the problem on its own, in the last check before pushing: that workflow triggers on changes to content paths, <code>website/docs/**</code>, <code>website/blog/**</code>, and so on. The new commit touched only the config file, the schema, and the workflow itself. None of those matched, so the workflow would have been skipped, not failed. I would have had a green pull request, a check that never ran, and no way to tell the difference from the outside. A check that doesn't run is worse than no check, because the green tick still shows up.</p>
<p><strong>A rule that had been failing invisibly.</strong> Vale started complaining about spaced em dashes in a line the metadata work had just added, and I said so, mostly because I wanted the rule gone. Claude went looking and came back with something I hadn't asked about: that same rule was already firing fourteen times in <code>pipeline.md</code>, on text that has been sitting there for months. I had never seen a single one, because reviewdog only comments on lines inside the diff. Those lines hadn't changed since the rule landed, so the errors were real, error level, and completely invisible. My linter had been finding things and telling nobody.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-actually-changed">What I actually changed<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#what-i-actually-changed" class="hash-link" aria-label="Direct link to What I actually changed" title="Direct link to What I actually changed" translate="no">​</a></h2>
<p>Descriptions now exist on every published page, and <code>pipeline.md</code> finally has front matter of its own. Both new files are committed to the repo, so the schema and config are as open to inspection as everything else here.</p>
<p><code>last_reviewed</code> takes its value from each file's last commit date, not from today's date. Claude proposed that and I agreed immediately, because stamping today would have asserted that someone reviewed twenty-eight files that afternoon. Nobody did. They were read, which is a different claim, and writing the stronger one into metadata would have been the exact failure this check exists to catch, committed on day one by the people installing the check.</p>
<p>Two files also dropped out of validation: the starter blog posts Docusaurus ships with, still sitting in my repo full of placeholder text. Docusaurus already excludes them from the build, and Claude flagged that writing descriptions for pages nobody publishes would be inventing metadata to satisfy a number. That was the right call. The new check's first effect was on the scope of what gets checked, not on any file.</p>
<p>The result:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">28 files checked, 28 passed, 0 failed, 0 errors</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-a-tool-is-never-one-change">Adding a tool is never one change<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#adding-a-tool-is-never-one-change" class="hash-link" aria-label="Direct link to Adding a tool is never one change" title="Direct link to Adding a tool is never one change" translate="no">​</a></h2>
<p>The pipeline had four tools. Now it has five, and that number is written down in more places than I remembered: the tool table on the pipeline page, a second independent table in the README, the sentence listing the gates on <a class="" href="https://oddlittlebird.github.io/showcase-site/how-i-built-this">How I built this</a>, the page describing the Doc Detective test, and the test itself, which asserts that the pipeline page names every tool it claims to run. That test needed a fifth assertion, or it would have kept passing while checking four fifths of the truth.</p>
<p><code>llms.txt</code> needed updating too, but not by hand. It's generated from a template plus live repo facts, which is the fix I wrote about in <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact">A baseline is not a fact</a>. The counts can't drift there anymore. The prose around them still can, so the template needed the new gate written into it.</p>
<p>Then the part I didn't expect: <code>scripts/pipeline-facts.json</code> feeds the generated file, and it claimed my terminology rule errors on "AI agent." It didn't, and it hadn't for a while. It also covered a term the file never mentioned, <code>LLM</code>, which I have since dropped from the rule so I can decide that one case by case. So <code>llms.txt</code> had been handing AI agents a wrong description of my own linter, generated correctly from a source that was quietly false. (sigh)</p>
<p>I built the generator specifically so that facts couldn't drift, and it works. What it checks is that every rule file has an entry and every entry has a rule file. It compares filenames. It never opens the rule to see whether the sentence about it is true. The counts can't drift, because they're read off the filesystem every time. The descriptions can, because they're just sentences somebody typed once.</p>
<p>So it faithfully rendered a fact that had already drifted upstream of it. That's the more interesting failure, and it's the next thing I'm building. A substitution rule's swap list is structured data, so a script can pull the terms straight out of the rule and compare them against the terms the prose claims it flags. That only covers substitution rules. <code>Passive.yml</code>'s description is genuinely prose, and no script is going to verify it. But it closes the specific gap that let this one through.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-was-actually-for">What this was actually for<a href="https://oddlittlebird.github.io/showcase-site/blog/the-file-with-no-metadata-passed-every-check#what-this-was-actually-for" class="hash-link" aria-label="Direct link to What this was actually for" title="Direct link to What this was actually for" translate="no">​</a></h2>
<p>I should be straight about the stakes. This is a small personal site. I was fine with just titles, and I wasn't especially worried about descriptions. I ran docmeta mostly because I wanted to try it, and I'm glad I did.</p>
<p>A site like mine doesn't come with strict metadata requirements, but plenty of other sites do. For example, enterprise documentation sets, anything under regulatory obligations, anything where metadata decides retention or access or what turns up in an audit. Those have requirements that are written down somewhere and, more often than not, lack enforcement.</p>
<p>That's the gap worth naming, and it isn't that my blog posts lacked descriptions. It's that nothing in a pipeline with four other gates in it had an opinion about whether they did. An unchecked field doesn't announce itself. It sits there looking exactly like a checked one.</p>
<p>The requirements are also going to move. The more that what we write gets retrieved and summarized by AI agents rather than read by people, the more the definition of adequate metadata will shift, and I don't think anyone knows yet where it will settle.</p>
<p>So this was a proof of concept, and it took under two minutes to set up. It's a fun little tool, so I suggest you give it a spin, especially if you're working on documentation with front matter requirements.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The reviewer's first catch was its own builder]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder"/>
        <updated>2026-08-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The staleness reviewer I built caught an unverified assumption on its first real run, and what it caught was baked into its own design.]]></summary>
        <content type="html"><![CDATA[<p>The monthly reviewer I built to catch stale content caught something on its first real run. It wasn't a stale file; it was an assumption I (through Claude) had been carrying for weeks, unverified, since I fixed a dead link and moved on.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-started">Where this started<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#where-this-started" class="hash-link" aria-label="Direct link to Where this started" title="Direct link to Where this started" translate="no">​</a></h2>
<p>This started as a pattern, not a plan. While Claude and I were updating other pipelines and tests, I kept having to remind it to also update some related file it hadn't touched.</p>
<p><code>CLAUDE.md</code>'s own rule about keeping <code>pipeline.md</code> honest is what that rule exists to prevent. Even so, it needed a reminder to update related files, like <code>llms.txt</code> and "How I built this," which also describe the pipeline.</p>
<p>I quickly realized that depending on me (and my fallible human brain) to remember which files were related, every time, wouldn't scale. Depending on Claude to remember on its own wouldn't either, as demonstrated by the fact that the rule in CLAUDE.md wasn't doing it.</p>
<p>Claude and I brainstormed a fix: <a class="" href="https://oddlittlebird.github.io/showcase-site/agents-and-skills/coupling-map">a small, maintained map</a> lists file pairs that state the same facts and could drift apart. <code>scripts/coupling-map.json</code> holds the pairs of related files. A new CI workflow runs on every pull request and diffs the changed files against it. It comments, only ever asking, whenever one side of a pair changed without the other.</p>
<p>That solved the original problem and created another problem, albeit a smaller one. The map is yet another file someone has to remember to keep current. I mean, it worked for now, but the site and pipeline will continue to expand and evolve, so the dependencies that need mapping will, too.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-reminder-for-the-reminder">A reminder for the reminder<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#a-reminder-for-the-reminder" class="hash-link" aria-label="Direct link to A reminder for the reminder" title="Direct link to A reminder for the reminder" translate="no">​</a></h2>
<p>The map needed its own periodic check. Monthly felt right, but that meant answering a harder question first: how do you automate remembering to check this? Both the human and the LLM doing the remembering are the exact failure mode being fixed.</p>
<p>The lowest-effort option was a reminder on my own calendar. It would probably work, if I paid attention to it. However, it would also live entirely outside the pipeline. This site exists to show a working pipeline that catches its own drift, not one that hands the hardest part back to an external calendar app.</p>
<p>The better option was to add something inside the pipeline, on a schedule, that reads the map and the repo. It reports what it finds the same way this site's other tests already do, by opening a GitHub issue. This would require a language model to make the actual judgment call, not a fixed script.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cloud-routine-that-went-nowhere">The cloud routine that went nowhere<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#the-cloud-routine-that-went-nowhere" class="hash-link" aria-label="Direct link to The cloud routine that went nowhere" title="Direct link to The cloud routine that went nowhere" translate="no">​</a></h2>
<p>I had Claude build a scheduled routine to run that check once a month. It would read the map, check whether its entries still hold, and look for new pairs worth adding. I then ran it manually to test it before waiting for the real schedule.</p>
<p>Nothing happened.</p>
<p>I ran it again. Silence.</p>
<p>The tool I had for managing that routine could list it, fetch its config, and trigger a run. It couldn't show me a transcript, a log, or an error: no setting to flip, nothing hidden behind a flag, just nothing there. Two runs, zero information about why either one failed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="switching-to-something-debuggable">Switching to something debuggable<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#switching-to-something-debuggable" class="hash-link" aria-label="Direct link to Switching to something debuggable" title="Direct link to Switching to something debuggable" translate="no">​</a></h2>
<p>We seemed stuck, at least with the current path. I asked whether we could use the mechanism Doc Detective's workflows already use to open issues: plain <code>gh issue create</code> against the repo's own token. That would sidestep the cloud routine's GitHub access problem entirely.</p>
<p>Claude took that further than I'd actually asked. The whole task could run as a GitHub Actions workflow, using Anthropic's own <code>claude-code-action</code>, not just the reporting step. No separate app dependency needed at all.</p>
<p>That mattered for a second reason too. A failure here now shows up as a normal Actions log, like every other workflow in this pipeline. Unlike the cloud routine, it's not a black box with nothing to debug.</p>
<p>Before writing the YAML, Claude checked the action's actual <code>action.yml</code> directly. Two different documentation sources had disagreed on whether its <code>claude_args</code> input took JSON or command-line flags.</p>
<p>Turns out, it's flags, passed straight to the Claude CLI. Trusting prose over the source would have cost a wasted run finding that out the hard way. (And because the new method required Anthropic API, each run would cost me a small amount of money, so fewer runs meant less $$.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="three-wrong-guesses-one-real-answer">Three wrong guesses, one real answer<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#three-wrong-guesses-one-real-answer" class="hash-link" aria-label="Direct link to Three wrong guesses, one real answer" title="Direct link to Three wrong guesses, one real answer" translate="no">​</a></h2>
<p>The first real run failed with <code>Claude execution failed: result is_error:true</code>. Nothing else.</p>
<p>Claude had one real lead. The <code>--allowedTools</code> pattern used a colon, <code>Bash(gh issue:*)</code>, while the CLI's own <code>--help</code> only ever showed that syntax with a space. It fixed the pattern and asked for a re-run.</p>
<p>Second run, same generic failure, but this time the full log came back. It ruled out Claude's own fix: the run lasted 409 milliseconds, one turn, zero dollars spent, zero permission checks. It had failed before a tool-permission pattern would ever matter.</p>
<p>The log also explained why the real error had been invisible: the action hides its own output behind a <code>show_full_output</code> flag, which was off by default. And it named the model it had actually used, <code>claude-opus-5</code>. I hadn't realized I needed to specify a model, so I never told Claude which one to use. Because I hadn't, neither did it, and the action defaulted to the most expensive option instead of the Sonnet pricing we'd already discussed. (Thanks, Anthropic!)</p>
<p>Claude turned on full output and pinned the model to Sonnet. Third run: the real error, finally visible. <code>Credit balance is too low</code>.</p>
<p>The API key worked. It just had no funded balance behind it, an ordinary billing gap hidden behind two layers of suppressed output until we finally saw it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-actually-found">What it actually found<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#what-it-actually-found" class="hash-link" aria-label="Direct link to What it actually found" title="Direct link to What it actually found" translate="no">​</a></h2>
<p>Once billing was sorted, the workflow ran clean and opened its first real issue. (Yay!) The two existing map entries checked out: it re-ran the actual count commands against the live repo and confirmed both pairs still matched (<code>ls .github/workflows | wc -l</code>, <code>ls .vale/styles/Diana | wc -l</code>). It proposed four new pairs.</p>
<p>Two of the four were solid on inspection. <code>.claude/commands/check.md</code>'s exact shell commands are quoted verbatim in a separate write-up page, and <code>pipeline.md</code>'s tool table is duplicated independently in <code>README.md</code>. Both went straight into the map.</p>
<p>The other two were both about the same thing. <code>intro.mdx</code> and a template file still said <code>skill.md</code>, a name that hadn't existed since a rename weeks earlier. Claude had seen that exact name before, in a different context, and it had already decided what that meant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-part-claude-got-wrong">The part Claude got wrong<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#the-part-claude-got-wrong" class="hash-link" aria-label="Direct link to The part Claude got wrong" title="Direct link to The part Claude got wrong" translate="no">​</a></h2>
<p>Weeks earlier, fixing the dead link this whole saga started with, Claude found <code>pipeline.md</code>'s own text calling itself "skill.md and llms.txt" in a heading. It read that as intentional, a nickname the file used for itself, and kept the label when it repaired the link that pointed at it.</p>
<p>It never asked whether that was actually true. It inferred, and then carried the inference forward as though it were settled. I'd read that reasoning when Claude wrote it and let the phrase "intentional site branding" go by without asking what it actually meant. One clarifying question, right then, would have caught it.</p>
<p>It wasn't settled. My correction was one sentence: "skill.md refers to Claude skills, not to pipeline.md, and never did."</p>
<p>The heading itself had been the drift, sitting there since the rename. Claude's fix from weeks earlier had quietly preserved it instead of questioning it. That's the same failure the last post was about, just moved from code into Claude's own reasoning: a baseline only proves a file agrees with its own history.</p>
<p>An unverified assumption only proves Claude agreed with itself. Neither one checks against what's actually true, and both will hold up as long as nobody asks.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-changed">What actually changed<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#what-actually-changed" class="hash-link" aria-label="Direct link to What actually changed" title="Direct link to What actually changed" translate="no">​</a></h2>
<p>Every current-state reference to "skill.md" is now "pipeline.md": the heading, the intro page, both spots in the template. The historical references stay as they were, because they're describing the old name accurately, in the past tense, as part of an incident that already happened. The map grew by two real pairs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-this-scale">Does this scale?<a href="https://oddlittlebird.github.io/showcase-site/blog/the-reviewers-first-catch-was-its-own-builder#does-this-scale" class="hash-link" aria-label="Direct link to Does this scale?" title="Direct link to Does this scale?" translate="no">​</a></h2>
<p>This site is a proof of concept for a documentation pipeline with real testing behind it, not a demo. So it's worth asking honestly: does any of this actually scale past one small repo with one contributor?</p>
<p>"If you touch this, also check that" is not a new problem. Larger teams deal with it constantly, across far more files than this repo has. Traditionally, that knowledge has lived in the heads of the writers and engineers who've been around long enough to remember it, not in any system.</p>
<p>That's fine until someone leaves, or until enough time passes that even the person who remembers stops trusting their own memory. As more of a pipeline gets automated, that knowledge needs somewhere more durable to live. The parts of the process that used to force a human to notice, someone reading every diff, are the parts getting automated away.</p>
<p>The monthly reviewer runs again in September. What it's actually testing, every time, is whether a small piece of that tribal knowledge can live in a file instead of a person. And whether anyone still reads the result closely enough to catch what it gets wrong.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A baseline is not a fact]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact"/>
        <updated>2026-08-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[For seven weeks my llms.txt claimed three Vale rules where there were five, and the test I built to catch exactly that kept passing.]]></summary>
        <content type="html"><![CDATA[<p>For seven weeks, my site's <code>llms.txt</code> told an AI agent my pipeline ran three custom Vale rules. It ran five. The test I'd built specifically to catch that kind of thing kept passing the whole time.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-happened">What actually happened<a href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact#what-actually-happened" class="hash-link" aria-label="Direct link to What actually happened" title="Direct link to What actually happened" translate="no">​</a></h2>
<p>Three separate, unrelated commits landed between late May and mid-June:</p>
<ul>
<li class=""><strong>2026-05-27</strong> — I swapped the API reference renderer from Redoc to Scalar.</li>
<li class=""><strong>2026-06-04</strong> — I renamed <code>skill.md</code> to <code>pipeline.md</code>.</li>
<li class=""><strong>2026-06-15</strong> — I added two new Vale rules, <code>Acronyms.yml</code> and <code>Headings.yml</code>, bringing the total from three to five.</li>
</ul>
<p>None of those commits touched <code>llms.txt</code>. Each one was a correct, complete change to the thing it was actually about: a plugin swap, a file rename, a new lint rule. None of them had any reason to know that a separate file elsewhere in the repo was quietly restating facts that had just become wrong.</p>
<p>By the time I noticed, <code>llms.txt</code> was claiming three Vale rules where there were five, and five workflows where there were eleven. It still credited Redoc for a renderer it hadn't used in over two months. It also linked to <code>/docs/skill.md</code>, a URL that had 404'd since the rename.</p>
<p>Nobody caught any of it, because nobody reads <code>llms.txt</code>. That's not a bug. It's the whole design of the file: it exists for an AI agent, not a person.</p>
<p>I'd already written a <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines">post about exactly that asymmetry</a>. Which made it more uncomfortable, not less, that the test I built <em>because</em> of that asymmetry didn't catch this either.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-test-that-already-existed">The test that already existed<a href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact#the-test-that-already-existed" class="hash-link" aria-label="Direct link to The test that already existed" title="Direct link to The test that already existed" translate="no">​</a></h2>
<p>That earlier post described a real mechanism. A script counts <code>llms.txt</code>'s <code>##</code> headings and writes the count to a baseline file. A Doc Detective test then compares the live file's heading count against that baseline on every run.</p>
<p>If they match, pass. If the live file has drifted from its last known-good state, fail.</p>
<p>It's a reasonable design, and I stand by the reasoning in that post for why I chose it over hardcoding a number. What I didn't realize at the time: "drift from its last known-good state" meant the baseline <em>was</em> the last version of <code>llms.txt</code>. Every time the count script ran, it recorded whatever the file currently said, headings and all.</p>
<p>So the test wasn't checking "does this file still match reality." It was checking "does this file still match itself." Those are different claims, and <code>llms.txt</code> can satisfy the second one perfectly forever while failing the first one from the moment the underlying facts change.</p>
<p>None of the three commits above added or removed a section heading. Each one changed a number in a bullet list, a link target, or a tool name in a sentence — content that lives <em>under</em> a heading. A test built to catch drift was, by construction, blind to the exact shape of drift that happened.</p>
<p>I'll say directly what I got wrong here, because it's worth naming rather than smoothing over. I was under the impression that the test compared <code>llms.txt</code>'s heading count against the live page or pages it describes. I didn't catch this until I sat down and closely re-read the test's own write-up.</p>
<p>It doesn't, and never did. It compares the file to a stored copy of a previous version of itself. That's a meaningfully different claim.</p>
<p>The write-up wasn't ambiguous by accident. I just hadn't reread it critically enough to notice which noun "baseline" was doing a lot of work for.</p>
<p>Diligence here doesn't mean trusting that a test does what its name suggests. It means rereading closely enough to be sure, especially when an assistant wrote both the test and its own description of it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="three-ways-to-fix-it">Three ways to fix it<a href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact#three-ways-to-fix-it" class="hash-link" aria-label="Direct link to Three ways to fix it" title="Direct link to Three ways to fix it" translate="no">​</a></h2>
<p>Once I saw the actual gap, I looked at three ways to close it:</p>
<ul>
<li class="">
<p><strong>Keep the self-comparison, widen the scope.</strong> Instead of comparing <code>llms.txt</code> to its own last version, compare it against the heading count summed across every page it links to. This is the option I considered first — it sounds obviously right, since a change in the pages <code>llms.txt</code> describes should show up somewhere. But it doesn't hold up under the same test I just described: none of the three drifts changed a heading anywhere on the site. A tool swap, a file rename, and two new lint rules all landed as prose and link edits, not new sections. Summing across more pages doesn't fix a check that's counting the wrong kind of thing. It just makes the noise louder. An unrelated blog post can shift the total and break the baseline for no real reason.</p>
</li>
<li class="">
<p><strong>Drop headings entirely, check the actual claims.</strong> <code>llms.txt</code>'s riskiest content isn't its structure. It's a small number of specific facts: how many Vale rules exist, how many workflows exist, which tool renders the API reference. Those are all things I can compute directly from the repository, the same way <code>pipeline.md</code> already gets self-checked (<code>ls .vale/styles/Diana | wc -l</code>, <code>ls .github/workflows | wc -l</code>). I'd already written that habit into this repo's <code>CLAUDE.md</code>, after an external collaborator caught <code>pipeline.md</code> undercounting its own workflows months earlier. Comparing the live file against a freshly computed fact, rather than a stored snapshot, catches all three incidents. It's the only one of the three designs that would. That's because it doesn't depend on the file ever having been right to begin with.</p>
</li>
<li class="">
<p><strong>Do nothing, accept the gap.</strong> Worth naming as an option even though I didn't take it: <code>llms.txt</code> isn't load-bearing infrastructure. A slightly stale index file on a portfolio site is a low-stakes place to under-invest. I didn't take this option because the whole thesis of this pipeline is that a well-structured file with wrong information is worse than no file. Leaving that thesis unenforced on the one file written specifically to demonstrate it felt like the wrong place to slack off.</p>
</li>
</ul>
<p>I went with the second option.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-shipped">What actually shipped<a href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact#what-actually-shipped" class="hash-link" aria-label="Direct link to What actually shipped" title="Direct link to What actually shipped" translate="no">​</a></h2>
<p><code>website/static/llms.txt</code> is no longer edited as static text. <code>scripts/generate-llms-txt.py</code> renders it from a template plus whatever the repository currently contains. The Vale rule count and names come from listing <code>.vale/styles/Diana/</code>; the workflow count comes from listing <code>.github/workflows/</code>.</p>
<p>Content that isn't a fact you can list stays hand-written in <code>scripts/pipeline-facts.json</code> and the template, on purpose. That includes what each Vale rule does in plain English, which pages are worth linking, and the bio. Some parts of a file like this should keep requiring a human.</p>
<p>Here's the part I'm more interested in than the generation itself. The generator refuses to run if a Vale rule has no matching description in the facts file. Add a sixth rule and forget to describe it, and nothing gets silently published with five rules listed instead of six.</p>
<p>The build just stops, with a message telling you exactly which file needs an entry. That's the actual fix for what happened in May and June.</p>
<p>It doesn't rely on remembering to update a second file when the first one changes. It makes forgetting loud instead of silent.</p>
<p>Testing the new file against the live site turned up a second gap I hadn't gone looking for. This site already runs <a href="https://lychee.cli.rs/" target="_blank" rel="noopener noreferrer" class="">Lychee</a> across every link on every page, <code>llms.txt</code> included. It never once flagged the dead <code>/docs/skill.md</code> link, across two months and roughly nine scheduled runs.</p>
<p>Its config resolves root-relative paths against a <code>localhost</code> base, so Docusaurus routes don't get treated as broken external URLs. That same trick means every root-relative link on the site, not just this one, is invisible to it by construction.</p>
<p>Rather than touch a site-wide config that's doing something deliberate for a reason, the new test includes a small, narrowly scoped check. It extracts every root-relative link inside <code>llms.txt</code> and confirms each one resolves. It's the same shape of decision as <a class="" href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check">an earlier test on this site</a> that does the same narrow-scope-on-purpose thing for a different reason.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-this-scale">Does this scale?<a href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact#does-this-scale" class="hash-link" aria-label="Direct link to Does this scale?" title="Direct link to Does this scale?" translate="no">​</a></h2>
<p>The generator runs as its own workflow, triggered by changes to the specific files that feed it, plus a daily schedule as a backstop. It's not folded into the main site build.</p>
<p>Every other content-generating workflow on this site already works this way, and I kept the pattern rather than reinventing it. <code>doodle.yml</code>, <code>api-reference.yml</code>, and the rest are all decoupled from <code>deploy.yml</code>, wired together only through <code>workflow_run</code> triggers after each one succeeds.</p>
<p>The reason is blast radius. If generation ran as a step inside <code>npm run build</code>, a bug in the generator could fail the entire site's deploy. That bug could be as small as a bad template placeholder or an unhandled edge case in the fact-counting.</p>
<p>A typo in a file almost nobody reads shouldn't be able to take the whole site offline. Keeping it as a separate job means the worst case of a broken generator is a red badge on one page. That's exactly the size of problem it actually is.</p>
<p>The generator was already working when a good question stopped me: were we making this harder than it needed to be? Two existing Docusaurus plugins looked like they might already do this: <code>docusaurus-plugin-llms</code> and <code>docusaurus-plugin-llms-txt</code>. Both do the same thing: scan the <code>/docs</code> folder and concatenate it into a single file.</p>
<p>Neither has any way to know how many Vale rules exist or how many workflows exist. Those aren't docs-page content — they're facts about the repo's tooling that live in <code>.vale/styles/Diana/</code> and <code>.github/workflows/</code>. Adopting either would have meant losing the specific claims that caused all three incidents in this post, not fixing them.</p>
<p>The maintenance picture made the decision easier, not harder. <code>docusaurus-plugin-llms-txt</code> hasn't been touched since late 2024, with four open issues sitting unanswered for months.</p>
<p><code>docusaurus-plugin-llms</code> is actively maintained, but its own issue tracker has an open bug, filed days before I checked. Content carried in a component prop gets silently deleted during generation, with no error and no warning. The reporter's own words: the site renders fine, the generated file is well-formed, "it just has less in it than the source did."</p>
<p>That's the exact failure category this whole post is about, just relocated into a dependency I don't control. I can't fix someone else's plugin in the next ten minutes the way I can my own code.</p>
<p>That's also the honest limit of how far this scales. Every new fact <code>llms.txt</code> states falls into one of two categories. Some are derivable from the filesystem — the generator can absorb those, and this whole class of drift goes away.</p>
<p>Others require a human judgment call, like what a rule actually does in plain English, and those live in <code>pipeline-facts.json</code>. The generator checks that an entry exists, not that it's still accurate.</p>
<p>It refuses to run if a rule has no description at all. But it has no way to notice a description that's gone stale next to a rule whose behavior quietly changed underneath it. That's a real gap, just a smaller and more honest one than the file-wide version this post started with.</p>
<p>The fix doesn't eliminate the need to remember. It shrinks what you have to remember. Instead of "does this whole file still describe a system that keeps changing," the question is now "does this one sentence describe this one rule."</p>
<p>And it turns the coarser failure — adding a rule and never describing it at all — into a build failure. That's better than a silence that lasts until someone reads closely enough to notice.</p>
<p>The other habit worth keeping, alongside the mechanism: a test doesn't get to stay trusted forever just because it's still green. This one sat unexamined for seven weeks before I looked at it closely enough to see what it actually checked.</p>
<p>A baseline only tells you a file agrees with its own history. It has never once told you the file was right to begin with.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Same claim, one more check]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check"/>
        <updated>2026-08-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The link test I had planned for months turned out to duplicate coverage Lychee already had. Redundant tests pass reliably and cover nothing.]]></summary>
        <content type="html"><![CDATA[<p>I had a Doc Detective test planned for months: point <code>checkLink</code> at the work samples on my <a class="" href="https://oddlittlebird.github.io/showcase-site/portfolio">portfolio page</a>, same as I do for every other link on this site with <a href="https://lychee.cli.rs/" target="_blank" rel="noopener noreferrer" class="">Lychee</a>. Then I sat down to actually build it, and realized the plan didn't hold up.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-plan-that-sounded-right-until-i-looked-harder">The plan that sounded right until I looked harder<a href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check#the-plan-that-sounded-right-until-i-looked-harder" class="hash-link" aria-label="Direct link to The plan that sounded right until I looked harder" title="Direct link to The plan that sounded right until I looked harder" translate="no">​</a></h2>
<p>The idea came from a list I'd sketched out early on: ten-plus Doc Detective showcases, each demonstrating a different capability. One line on that list read almost exactly like this: portfolio link check, <code>checkLink</code> on the work samples, blog post about running two link-checking tools side by side. It sounded like a clean parallel. Lychee already crawls every link on this site on a schedule; a Doc Detective test doing the same thing on a smaller, curated set felt like an obvious complement.</p>
<p>Then I went to actually build it and pulled up <code>portfolio.mdx</code> to see what I'd be pointing <code>checkLink</code> at. Every work sample is a static PDF or markdown file sitting in this repo's own <code>static/portfolio/</code> folder. Nothing is external link, so there's nothing that can change the way a link to someone else's site can.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-that-mattered">Why that mattered<a href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check#why-that-mattered" class="hash-link" aria-label="Direct link to Why that mattered" title="Direct link to Why that mattered" translate="no">​</a></h2>
<p>Lychee's job is asking one question of every link on this site: does this URL return a status code that I'm willing to accept? It doesn't care what's behind the link, only whether a server answered. For the portfolio's work samples, that question was already being asked, on the same links, on the same schedule. Writing a <code>checkLink</code> test that asked it again would have meant CI time spent re-confirming something already confirmed. Not a wrong test, but a redundant one.</p>
<p>Redundant tests are their own kind of quiet failure. They pass reliably, add a badge to a page, and let you feel like you've covered something you haven't actually added coverage for. If I'd shipped the plain version, then I'd have a page that looked exactly as thorough as the one I ended up with.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-question-lychee-cant-ask">The question Lychee can't ask<a href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check#the-question-lychee-cant-ask" class="hash-link" aria-label="Direct link to The question Lychee can't ask" title="Direct link to The question Lychee can't ask" translate="no">​</a></h2>
<p>A status code doesn't tell you what's actually behind a link. A PDF that got corrupted mid-upload, a file that got accidentally overwritten with zero bytes, a misconfigured redirect that serves a generic error page at a real 200, all of those pass Lychee's check without complaint. The response came back. The number was fine. Lychee has no way to open the file it just confirmed exists.</p>
<p>That gap is the one worth testing, so that's what the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/portfolio-link-test">portfolio link check</a> actually does. It still runs <code>checkLink</code> against each work sample, six of them, the same claim Lychee makes. Then it goes one step further: a shell script fetches each file and checks whether it's real. For the PDFs, that means reading the first four bytes and confirming they spell <code>%PDF</code>, plus a minimum file size well below what any of the actual documents weigh in at, so a truncated or emptied file can't sneak through. For the CockroachDB tutorial, it's a markdown heading and its own size floor. None of that is possible from a status code alone.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-two-tools-different-jobs-actually-looks-like">What "two tools, different jobs" actually looks like<a href="https://oddlittlebird.github.io/showcase-site/blog/same-claim-one-more-check#what-two-tools-different-jobs-actually-looks-like" class="hash-link" aria-label="Direct link to What &quot;two tools, different jobs&quot; actually looks like" title="Direct link to What &quot;two tools, different jobs&quot; actually looks like" translate="no">​</a></h2>
<p>I'd been picturing two tools running the same check on different scopes; Lychee everywhere, Doc Detective on a smaller set. What I built instead is two tools running different checks on the same links: Lychee asks whether the door opens, Doc Detective checks whether there's a room behind it. The overlap in scope, both tools do touch these six files, is fine. It's the overlap in <em>question</em> that would have been the problem.</p>
<p>The test that's live now doesn't prove more coverage exists on this site than actually does. It proves one specific thing an existing tool structurally can't: that a 200 response and a real, intact document are not the same fact, and a portfolio page is exactly the kind of place where nobody notices when they quietly stop being the same fact.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The parts of my pipeline that think, and the parts that don't]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont"/>
        <updated>2026-07-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why I reach for a deterministic linter in some places and a Claude Code skill in others, and what each layer actually buys me.]]></summary>
        <content type="html"><![CDATA[<p>I've had three custom Claude Code skills and a CLAUDE.md file running in this pipeline for weeks. Both live in the repo itself, not on my local machine. For the specifics of how they work, see <a class="" href="https://oddlittlebird.github.io/showcase-site/agents-and-skills/skills">Skills</a> and <a class="" href="https://oddlittlebird.github.io/showcase-site/agents-and-skills/claude-md">CLAUDE.md</a>. This post is about something those pages don't cover: why I reach for one over the other, and what each one is actually buying me.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-deterministic-layer">The deterministic layer<a href="https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont#the-deterministic-layer" class="hash-link" aria-label="Direct link to The deterministic layer" title="Direct link to The deterministic layer" translate="no">​</a></h2>
<p>The first thing I added to this pipeline were linters: Vale, codespell, Lychee. All three run on every pull request. Same input, same output, every time. That's what deterministic means here. They catch style violations, misspellings, and dead links, at the last possible moment, right before something merges. I don't think about them day to day. They just run.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-non-deterministic-ai-layer">The non-deterministic (AI) layer<a href="https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont#the-non-deterministic-ai-layer" class="hash-link" aria-label="Direct link to The non-deterministic (AI) layer" title="Direct link to The non-deterministic (AI) layer" translate="no">​</a></h2>
<p>CLAUDE.md rules work alongside the linters, but they enforce correctness a different way. A linter matches a pattern. CLAUDE.md gives Claude Code a judgment call to make, and judgment calls don't come back identical every time the way a regex match does.</p>
<p>Right now, there are two rules in CLAUDE.md:</p>
<ul>
<li class="">When I change anything the pipeline overview page describes, CLAUDE.md tells Claude Code to check that <code>pipeline.md</code>, <a class="" href="https://oddlittlebird.github.io/showcase-site/how-i-built-this">How I built this</a>, and this repo's README still describe it accurately, and fix whichever one drifted.</li>
<li class="">When I add a new Doc Detective test page, CLAUDE.md tells Claude Code to match the heading structure the other pages in that section already use, rather than inventing a new shape.</li>
</ul>
<p>Both rules exist because I got burned once. An external reader (not Claude) found the pipeline page undercounting its own workflows and Vale rules. Writing the second rule, I found a live example of the same kind of drift myself: one test page was missing its own title heading, and nobody had noticed. No linter catches either of those. They're not typos. They're a claim quietly going stale.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-efficiency-layer">The efficiency layer<a href="https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont#the-efficiency-layer" class="hash-link" aria-label="Direct link to The efficiency layer" title="Direct link to The efficiency layer" translate="no">​</a></h2>
<p>The skills don't enforce anything. They save time. Three commands, three chores I used to do by hand:</p>
<ul>
<li class=""><code>/new-post</code> scaffolds a blog post with the slug, title, and frontmatter already filled in.</li>
<li class=""><code>/check</code> runs my linters locally, so I see what the PR check would catch before I open the PR, not after.</li>
<li class=""><code>/start</code> boots my local dev server, and checks first so I don't end up with two running by accident.</li>
</ul>
<p>None of these skills are impressive engineering. They just remove friction.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-dont-know-yet">What I don't know yet<a href="https://oddlittlebird.github.io/showcase-site/blog/the-parts-of-my-pipeline-that-think-and-the-parts-that-dont#what-i-dont-know-yet" class="hash-link" aria-label="Direct link to What I don't know yet" title="Direct link to What I don't know yet" translate="no">​</a></h2>
<p>Here's the honest gap: I don't know whether the skills or the CLAUDE.md rules save me tokens, as opposed to time. A skill runs a fixed set of steps. A CLAUDE.md rule asks Claude Code to notice something and decide what to do about it, which is a different kind of expensive. I haven't measured either one, and I wouldn't bet confidently on the answer in either direction.</p>
<p>What's saving you time in your own process right now? A skill, an agent rule, something else? I'd like to know.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[What Claude Code didn't notice]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice"/>
        <updated>2026-07-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[My AI coding assistant spent days describing a test that was never running. How I caught it, and what it says about who still has to do the noticing.]]></summary>
        <content type="html"><![CDATA[<p>My AI coding assistant spent days confidently describing a test that wasn't actually running. Here's how I caught it — and what that says about who still needs to be doing the noticing.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="picking-a-target-that-actually-changes">Picking a target that actually changes<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#picking-a-target-that-actually-changes" class="hash-link" aria-label="Direct link to Picking a target that actually changes" title="Direct link to Picking a target that actually changes" translate="no">​</a></h2>
<p>I didn't want to reinvent the wheel, so I built on two existing image tests:</p>
<ul>
<li class="">the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference test</a>
and</li>
<li class="">the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/doodle-test">doodle test</a></li>
</ul>
<p>Claude Code suggested pairing the API reference test with the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/responsive-test">responsive screenshot test</a> instead. That probably would have worked, but I wanted to isolate the doodle specifically. My API sample won't change very often, and never without my actions. Google's doodle definitely will.</p>
<p>That distinction turned out to matter more than I expected.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bug-claude-code-didnt-notice">The bug Claude Code didn't notice<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#the-bug-claude-code-didnt-notice" class="hash-link" aria-label="Direct link to The bug Claude Code didn't notice" title="Direct link to The bug Claude Code didn't notice" translate="no">​</a></h2>
<p>As we worked, Claude Code kept talking about the doodle test as if it changed every day. It hadn't noticed that the test wasn't actually updating on days Google wasn't running an animated doodle.</p>
<p>I suspected — and it confirmed — why: the old test cropped the screenshot to a specific element, <code>#hplogo</code>, before saving it. That element only exists on days Google is running a special doodle. On an ordinary day, the crop target isn't there, the step fails, and the run produces nothing. No red badge, no error thrown. Just silent, invisible failure.</p>
<p>Once I pointed that out, Claude Code checked the dates on the doodle images already committed to the repo and started investigating from there. The evidence was sitting in plain sight: multi-day gaps in the commit history, days at a time with no update, because the crop target simply wasn't there to find. Claude Code's live inspection of Google's homepage confirmed it: no <code>#hplogo</code>, and no fallback either. <code>#lga</code>, an older, once-standard Google logo-container ID, wasn't there. Just obfuscated, build-specific class names, no stable selector of any kind on a plain-logo day.</p>
<p>Claude Code's first suggestion was to stop cropping and just capture the full page. I countered: could we crop it after the fact instead, or use a narrower viewport? I didn't want my Google account photo or bookmarks bar showing up in a screenshot on a public site.</p>
<p>Claude Code checked, and it turned out not to matter — CI runs in a fresh, unauthenticated browser session, so there's no account or bookmarks to leak in the first place. We went with the full page, uncropped, at a fixed viewport instead of a specific element, so it doesn't depend on anything existing that might not be there.</p>
<p>That's a real shortcoming in AI: it only notices what you tell it to. It means we still need to be the observant ones, watching for things that aren't behaving as expected, and the imaginative ones, describing the edge cases worth checking in the first place.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-test-actually-does-now">What the test actually does now<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#what-the-test-actually-does-now" class="hash-link" aria-label="Direct link to What the test actually does now" title="Direct link to What the test actually does now" translate="no">​</a></h2>
<p>With the crop gone, the test needed a real comparison mechanism, which is the actual point of Test #5: Doc Detective's <code>maxVariation</code> option.</p>
<p>A screenshot step can take <code>maxVariation</code>, a threshold for how much of the image (0 to 1, as a fraction of pixels) is allowed to differ from the image already saved at that path, and <code>overwrite</code>, which decides what happens to that saved image. There are three modes, and they're less alike than they sound:</p>
<ul>
<li class=""><code>"true"</code> replaces the file every run and never compares anything.</li>
<li class=""><code>"false"</code> captures once, then skips every later run outright, no comparison at all.</li>
<li class=""><code>"aboveVariation"</code> is the only mode that compares every run: it diffs the new capture against the saved one, and only replaces the saved image if the difference crosses the threshold.</li>
</ul>
<p>That third mode is what both tests use now. The doodle screenshot compares against its reference image at a 10% threshold. Most days, nothing crosses it. On doodle days, it does, and the reference image updates. The API reference screenshot got the same treatment at a tighter 2% threshold, since that page should barely ever change — a comparison flag there is a much stronger signal.</p>
<p>One more detail worth knowing if you use this yourself: exceeding <code>maxVariation</code> doesn't fail the test. It reports WARNING. That's a deliberate severity choice in Doc Detective, not a gap in my setup — "this looks different" is information, not automatically breakage.</p>
<p>It didn't occur to Claude Code that a WARNING which can't redden a badge is also a WARNING nobody would ever see. I had to ask directly: if this doesn't fail and doesn't show up anywhere, how would I actually find out it happened? Once I asked, the fix was easy — both workflows now open, or comment on, a GitHub issue when a comparison exceeds its threshold. But the question had to come from me first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-investigation-found-a-second-bug">The investigation found a second bug<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#the-investigation-found-a-second-bug" class="hash-link" aria-label="Direct link to The investigation found a second bug" title="Direct link to The investigation found a second bug" translate="no">​</a></h2>
<p>Digging into why the doodle test was failing silently turned up something else: Doc Detective's CLI exits <code>0</code> no matter what happens in the test. Claude Code confirmed it directly, running a test it knew would fail on a bad URL and getting a clean exit code back anyway. There's no flag in the current version to change that.</p>
<p>That mattered here because every workflow on this site checks for failure with a pattern like <code>|| echo "failed=true"</code>, which depends on a non-zero exit code that, it turns out, never comes. I had Claude Code rewrite the workflows this test touches to read Doc Detective's own JSON results file directly instead of trusting the exit code, and it flagged the same fix as still needed for the rest of the site's workflows, since they're all quietly making the same assumption.</p>
<p>That was a fun find. I don't usually get to be this hands-on with testing. I filed it upstream as <a href="https://github.com/doc-detective/doc-detective/issues/674" target="_blank" rel="noopener noreferrer" class="">doc-detective/doc-detective#674</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="writing-the-page-took-more-rounds-than-the-test-did">Writing the page took more rounds than the test did<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#writing-the-page-took-more-rounds-than-the-test-did" class="hash-link" aria-label="Direct link to Writing the page took more rounds than the test did" title="Direct link to Writing the page took more rounds than the test did" translate="no">​</a></h2>
<p>When it came time to write up the test page itself, Claude Code dutifully drafted everything it had observed while we worked, the crop bug, the exit-code bug, the whole investigation, right there on the page.</p>
<p>Except no. Like an overeager intern, it got lost in the details. Every test page on this site follows the same shape: why it matters, the latest result, how it works, what makes it fail, how to run it yourself. That consistency is an important convention in technical documentation. A migration story about a bug that no longer exists doesn't belong in a page explaining what the test currently does — it belongs here, in a post, where the history is the content instead of noise on top of it.</p>
<p>It took a few more rounds of edits before the page said only what it needed to say.</p>
<p>Again: human input. AI-assisted workflows are genuinely useful, but they're fragile, and they still need someone deciding what belongs on the page and what belongs in the commit history.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-this-slop">Is this slop?<a href="https://oddlittlebird.github.io/showcase-site/blog/what-claude-code-didnt-notice#is-this-slop" class="hash-link" aria-label="Direct link to Is this slop?" title="Direct link to Is this slop?" translate="no">​</a></h2>
<p>Which brings me to this post. I'm skeptical of the "60% of long-form LinkedIn content is AI slop" stat that's been going around, mostly because the so-called AI detectors behind it have an atrociously high false-positive rate. But it got me wondering: does this post count as slop?</p>
<p>I wrote it with the help of an LLM. The test I'm writing about was built with one too, but the ideas are mine. This was collaborative, and I was in the driver's seat the whole time. Would this article add to the AI slop stat? What if I threw in a typo or twoe? Maybe a double  space?</p>
<p>Either way, I'll keep building these Doc Detective tests as long as I keep coming up with testing ideas. This one is #5 on a list of 11 — so far.</p>
<p>I'm open to suggestions if you have any ideas of what you want to see tested, network!</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The browser had a floor I didn't know about]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about"/>
        <updated>2026-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Two screenshots at two viewports turned into a crash in Doc Detective and a browser that quietly refused to render as narrow as I asked.]]></summary>
        <content type="html"><![CDATA[<p>I set out to write a test that takes two screenshots of the homepage: one small picture, one big picture. The idea was simple enough that I expected to spend most of my time on the page copy, not the test. Instead, I hit a crash in Doc Detective itself, and then a browser that quietly refused to be as narrow as I asked it to be.</p>
<p>Really, now. I do not expect that kind of recalcitrance from my apps.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-test-is-different">Why this test is different<a href="https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about#why-this-test-is-different" class="hash-link" aria-label="Direct link to Why this test is different" title="Direct link to Why this test is different" translate="no">​</a></h2>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/pipeline-test">pipeline test</a> and the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference test</a> each check a page at one fixed viewport. Neither would notice if a layout change broke the mobile experience while leaving desktop untouched, or the other way around. That's a real gap: the markup and the content can both be fine, and the page can still fall apart at a width nobody happened to be looking at.</p>
<p>The fix seemed straightforward: define two tests on one page, identical steps, different <code>runOn</code> viewport. Navigate to the homepage, take screenshots: one at 375×812, one at 1280×800.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-crash">The crash<a href="https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about#the-crash" class="hash-link" aria-label="Direct link to The crash" title="Direct link to The crash" translate="no">​</a></h2>
<p>The first time I ran it, Doc Detective printed its usual parsed-test summary, both tests looking exactly as I'd written them, and then:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Cannot read properties of undefined (reading 'forEach')</span><br></div></code></pre></div></div>
<p>No stack trace pointing at my file, no line number, nothing. I assumed I'd gotten the JSON wrong somewhere, so I stripped the test down to the smallest possible case: one <code>runOn</code> block, one browser, no viewport at all. Same crash. I removed <code>runOn</code> entirely and the test ran fine, screenshot and all.</p>
<p>So the crash wasn't about viewport, or about my specific config. It was <code>runOn</code> itself.</p>
<p>I read the installed package's source rather than guess further. The context-resolution code loops over <code>context.platforms</code> unconditionally:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">platforms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">platform</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>The schema marks <code>platforms</code> as optional. Nothing defaults it to an array when it's missing. Any <code>runOn</code> context that specifies only <code>browsers</code>, which is the natural thing to write when you don't care what OS the test runs on, hits <code>undefined.forEach</code> and dies. This is a genuine bug in the installed version (doc-detective 4.9.0), not a mistake in my markup.</p>
<p>The workaround was to always list <code>platforms</code> explicitly, even though the schema doesn't require it:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token property" style="color:#36acaa">"platforms"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"linux"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"mac"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"windows"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"browsers"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token property" style="color:#36acaa">"name"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"chrome"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"viewport"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token property" style="color:#36acaa">"width"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">375</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token property" style="color:#36acaa">"height"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">812</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>With that in place, both tests parsed, resolved, and ran.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-width-that-wasnt">The width that wasn't<a href="https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about#the-width-that-wasnt" class="hash-link" aria-label="Direct link to The width that wasn't" title="Direct link to The width that wasn't" translate="no">​</a></h2>
<p>Both screenshots saved. Both looked like real, distinct layouts. One had a collapsed hamburger nav, one had a full top bar. I checked the file dimensions to confirm before moving on, and the desktop one read exactly 1280×800, as requested. The mobile one read 500×812.</p>
<p>I'd asked for 375. I tried narrower values, 320, even smaller, and kept landing on exactly 500. I tried a wider value, 550, and got exactly what I asked for. That bracketed it precisely: something in my local browser automation setup enforces a 500-pixel-wide floor, silently, regardless of what viewport a test requests. Anything narrower gets rounded up. Anything at or above passes through untouched.</p>
<p>Nothing failed. Nothing logged a warning. The test reported success, because by its own definition, success just means the browser reached the page and a screenshot got saved. It never claimed to guarantee the exact width.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-page-says-now">What the page says now<a href="https://oddlittlebird.github.io/showcase-site/blog/the-browser-had-a-floor-i-didnt-know-about#what-the-page-says-now" class="hash-link" aria-label="Direct link to What the page says now" title="Direct link to What the page says now" translate="no">​</a></h2>
<p>I could have picked a viewport value this setup happens to honor and called it "mobile." That would have made the numbers line up and told a smaller truth. Instead, the <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/responsive-test">responsive screenshot test</a> still requests 375×812, because that's a real, common mobile breakpoint and the honest target, and it says plainly, right above the screenshots, that requested and rendered aren't always identical, and that the images are the ground truth, not the caption.</p>
<p>I only caught the gap because I checked the file dimensions instead of trusting the green result. The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference test</a> passed on a similar assumption a few days ago, and that one turned out fine. This one didn't. The only way to tell the difference in advance is to look.</p>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/responsive-test">responsive screenshot test</a> page shows the result and updates it every day:</p>
<ul>
<li class="">A pass/fail badge with the timestamp of the last run</li>
<li class="">A screenshot of the mobile-width homepage</li>
<li class="">A screenshot of the desktop-width homepage</li>
</ul>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[What the screenshot doesn't prove]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove"/>
        <updated>2026-07-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A screenshot test of a client-rendered API reference cost me an afternoon, and the fix was a smaller claim: check for a selector, not for text.]]></summary>
        <content type="html"><![CDATA[<p>As part of my continuing journey into documentation testing, I set out to write a test that takes a screenshot of a live API reference. It sounded simple: load the page, wait for it to render, take a picture. Then I asked Claude Code what the test was actually checking, and had to admit the honest answer was "not much."</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-different-kind-of-fragile">A different kind of fragile<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#a-different-kind-of-fragile" class="hash-link" aria-label="Direct link to A different kind of fragile" title="Direct link to A different kind of fragile" translate="no">​</a></h2>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/pipeline-test">pipeline test</a> checks static content, markdown that Docusaurus renders once at build time. The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference test</a> checks something that isn't part of this site's build output at all. Every time someone loads the <a class="" href="https://oddlittlebird.github.io/showcase-site/api">API reference</a>, <a href="https://scalar.com/" target="_blank" rel="noopener noreferrer" class="">Scalar</a> fetches a script from a CDN, fetches an OpenAPI spec, and renders the whole reference client-side, on the spot. Any of those steps can break independently of anything in this repo. A static content check would never catch that.</p>
<p>My first draft's plan was to confirm the page loaded, then screenshot it. That was also my first mistake, because I hadn't yet worked out what "confirm it loaded" needed to mean.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bug-that-ate-an-afternoon">The bug that ate an afternoon<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#the-bug-that-ate-an-afternoon" class="hash-link" aria-label="Direct link to The bug that ate an afternoon" title="Direct link to The bug that ate an afternoon" translate="no">​</a></h2>
<p>My first version of the test navigated to the page, then used Doc Detective's <code>find</code> step to check that the text "Garden companion API" appeared somewhere in the body, before taking the screenshot. It failed. Every time. Even at a 90-second timeout, which ruled out simple slowness.</p>
<p>I spent a long stretch assuming the problem was the browser, or the CDN, or a shadow DOM boundary hiding the text from a plain query. None of that held up. A screenshot taken a few seconds after page load, with no assertion at all, showed the heading rendered clearly, right where it should be. The content was there. The <code>find</code> step just couldn't see it.</p>
<p>The actual cause was narrower and stranger: matching against live text on a page that Scalar keeps re-rendering seems to break whatever polling mechanism Doc Detective uses to retry the check. The fix wasn't a longer timeout, it was a smaller claim. Instead of asking "does this page contain the text 'Garden companion API,'" I asked "does an element matching <code>h1.section-header-label</code> exist." Same page, same moment, a check on structure instead of content, and it passed immediately.</p>
<p>I don't fully know why the text-matching path fails on this specific page. I know the selector-based path doesn't, and I know which one to reach for next time a client-rendered page needs a check like this.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-second-bug-was-in-my-own-writing">The second bug was in my own writing<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#the-second-bug-was-in-my-own-writing" class="hash-link" aria-label="Direct link to The second bug was in my own writing" title="Direct link to The second bug was in my own writing" translate="no">​</a></h2>
<p>Once the render check passed, a different set of steps started failing, ones I hadn't written on purpose. Doc Detective doesn't just run the steps you write; it also scans the surrounding markdown for patterns that look like instructions, and turns them into steps automatically. My "run this test manually" section told a human reader to go to the <strong>Actions</strong> tab and click <strong>Run workflow</strong>. Doc Detective read that bold text as literal <code>find</code> and <code>click</code> targets, and tried to execute them against a page that had never heard of a "Run workflow" button.</p>
<p>The instructions for a human running the test by hand had become test steps of their own, silently, because of formatting I'd chosen for readability. The fix was to wrap that section in an explicit ignore marker, telling Doc Detective not to scan it. I later found the same bug sitting untouched in an earlier test on this site, unnoticed because its real steps had been failing for an unrelated reason and never got far enough to trigger it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-test-that-always-passes-is-worthless">A test that always passes is worthless<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#a-test-that-always-passes-is-worthless" class="hash-link" aria-label="Direct link to A test that always passes is worthless" title="Direct link to A test that always passes is worthless" translate="no">​</a></h2>
<p>Once I'd fixed both of those, the test ran cleanly, which is when I got asked the question that mattered: how does the test know the screenshot is valid?</p>
<p>It doesn't, not in any deep sense. The <code>find</code> check proves that <em>something</em> with the right shape rendered before the picture was taken. It does not check that the picture shows the right content, and Doc Detective saves the screenshot unconditionally, with no comparison against the last one. A visual regression that still produces the same heading element, broken styling, wrong body text, a layout that fell apart, would sail through untouched.</p>
<p>That's not a bug I fixed. It's a boundary I hadn't stated. The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/api-reference-test">API reference test page</a> now spells it out directly: the test fails only if Scalar's render pipeline produces nothing at all, not if it produces something wrong. Catching the second case is a different kind of test, one that diffs a new screenshot against a trusted baseline instead of just checking that a screenshot was possible. That's a real gap, and pretending otherwise would have made this test look more capable than it is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-environment-lied-more-convincingly-than-the-test-did">The environment lied more convincingly than the test did<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#the-environment-lied-more-convincingly-than-the-test-did" class="hash-link" aria-label="Direct link to The environment lied more convincingly than the test did" title="Direct link to The environment lied more convincingly than the test did" translate="no">​</a></h2>
<p>The last wrinkle had nothing to do with Doc Detective. My local sandbox couldn't run the finished test reliably, failing on browser automation bootstrap issues that had nothing to do with the test logic, three different infrastructure errors across three attempts, no zombie processes, no obvious cause. I'd already proven the underlying check worked in isolation, more than once, but I couldn't get a single clean end-to-end run locally to point to as proof.</p>
<p>The real proof came from GitHub Actions, which ran the finished workflow on a clean runner and passed on the first try, with a genuine screenshot and a status file to show for it. The lesson wasn't about Doc Detective. It was a reminder that a flaky local environment and a flaky test look identical from the inside, and the only way to tell them apart is a clean environment you trust.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-test-is-actually-for">What this test is actually for<a href="https://oddlittlebird.github.io/showcase-site/blog/what-the-screenshot-doesnt-prove#what-this-test-is-actually-for" class="hash-link" aria-label="Direct link to What this test is actually for" title="Direct link to What this test is actually for" translate="no">​</a></h2>
<p>This is the narrowest test in the series so far, and I mean that as a description, not a complaint. It proves that a third-party render pipeline, one entirely outside this repo's control, still produces output. It doesn't prove the output is correct, and now it says so, on the page, next to the badge.</p>
<p>A test that claims more than it checks is worse than no test at all, because it teaches you to stop looking. This one only promises what it can back up.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Testing for machines]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines"/>
        <updated>2026-07-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[My llms.txt file exists for AI agents, not readers, so nothing in a normal pipeline notices when it drifts. Here is the structural test I wrote instead of a content check.]]></summary>
        <content type="html"><![CDATA[<p>My portfolio site has an <code>llms.txt</code> file. It's not for you.</p>
<p><strong>Update (2026-08-04):</strong> The baseline-comparison test described in this post
has since been retired and rebuilt. It compared <code>llms.txt</code> against a stored
copy of its own previous version, which turned out not to catch the kind of
drift that actually happened. For a full breakdown of what changed and why. see
<a class="" href="https://oddlittlebird.github.io/showcase-site/blog/a-baseline-is-not-a-fact">A baseline is not a fact</a>.</p>
<!-- -->
<p><a href="https://llmstxt.org/" target="_blank" rel="noopener noreferrer" class=""><code>llms.txt</code></a> is the emerging convention for giving AI tools a structured, machine-readable map of a site's content. The idea is the same as <code>robots.txt</code>, but aimed at language models and AI-assisted search rather than web crawlers. The file describes what the site covers, where the key pages are, and who made it, in plain markdown that a retrieval pipeline or AI agent can fetch and pass along as context without rendering a browser.</p>
<p>Nobody clicks it. Nobody subscribes to it. If it disappears, or drifts out of sync with the site it's supposed to describe, no human notices. An AI agent might start returning wrong information about the site, and nothing in a normal deployment pipeline would catch it, because nothing in a normal deployment pipeline is watching a static file that no human is reading.</p>
<p>That's the problem I wanted to test.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-assert">What to assert<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines#what-to-assert" class="hash-link" aria-label="Direct link to What to assert" title="Direct link to What to assert" translate="no">​</a></h2>
<p>I worked with Claude Code to draft <a href="https://doc-detective.com/" target="_blank" rel="noopener noreferrer" class="">Doc Detective</a> tests. Claude's first suggestion was to assert on content: check that the file names a specific tool, or includes a particular heading, or contains the author's name.</p>
<p>I pushed back. Most documentation doesn't have a named author in the machine-readable index. And if you're maintaining <code>llms.txt</code> files across ten products, content assertions require hand-editing every time something changes — which means they quietly go stale, which is the exact problem you were trying to prevent.</p>
<p>The more useful assertion is structural. The <a href="https://llmstxt.org/" target="_blank" rel="noopener noreferrer" class=""><code>llms.txt</code> spec</a> requires an H1 title and at least one <code>##</code> section heading. Those are format requirements that hold across any site's implementation. A 200 response with no section headings is either a placeholder or a corrupted file, and you want to know either way.</p>
<p>The tool doesn't decide what's worth testing. That part is still a human judgment call.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="going-further-without-hardcoding">Going further without hardcoding<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines#going-further-without-hardcoding" class="hash-link" aria-label="Direct link to Going further without hardcoding" title="Direct link to Going further without hardcoding" translate="no">​</a></h2>
<p>Structural conformance catches a broken or missing file. It doesn't catch drift: a file that was valid six months ago and is still technically valid now, but no longer reflects the site's current shape because sections were added or removed.</p>
<p>The obvious fix is to count headings and assert on the count. The problem is that hardcoding a number is just a more subtle version of the same mistake. It starts failing the moment the structure changes intentionally, and whoever runs the test has to remember to update the number, which is manual maintenance that someone will eventually skip.</p>
<p>A baseline comparison works better. A script runs on every push that touches <code>llms.txt</code>, counts the headings by level, and writes the result to a JSON file in the repo. A Doc Detective test then fetches the live file, counts the headings, and compares against the stored baseline. If they match, the test passes. If the file has drifted from its last known-good state, the test fails.</p>
<p>When you intentionally add or remove a section, you update <code>llms.txt</code>, the count script runs, the baseline updates, and the test passes again. The test only fails when the live file diverges from what was last committed. The count is never hardcoded anywhere.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="making-it-visible">Making it visible<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines#making-it-visible" class="hash-link" aria-label="Direct link to Making it visible" title="Direct link to Making it visible" translate="no">​</a></h2>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/llms-txt-test">llms.txt verification test page</a> shows the live heading counts alongside the pass/fail badge. That's the same pattern as the pipeline test: don't leave the result buried in a GitHub Actions log where nobody looks until something breaks loudly. Put it next to the thing it's checking.</p>
<p>The heading counts also make the state of the file visible at a glance, independent of whether the test is passing. If the counts look wrong to a human reader, that's useful information regardless of what the automated assertion says.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-testing-surface-nobody-talks-about">The testing surface nobody talks about<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-for-machines#the-testing-surface-nobody-talks-about" class="hash-link" aria-label="Direct link to The testing surface nobody talks about" title="Direct link to The testing surface nobody talks about" translate="no">​</a></h2>
<p>Most documentation testing is aimed at human readers: page loads, links resolve, images render. Those are the things people notice when they go wrong.</p>
<p><code>llms.txt</code> has a different audience. It's read by retrieval pipelines and AI agents, not people — whatever sits in front of a chatbot that someone has pointed at your documentation. That audience won't open an issue when it gets a 404 instead of a site description. It just uses whatever it got.</p>
<p>Testing for that audience means thinking about what "correct" looks like from the pipeline's side, not the reader's. A test that checks whether <code>llms.txt</code> is reachable, structurally valid, and still has the right shape is a test written for someone who will never tell you when it's broken.</p>
<p>Not "does this page render well," but "is this file still describing something true, to a reader who won't complain if it isn't."</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Testing the pipeline description]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description"/>
        <updated>2026-06-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Treating a documentation page as a claim rather than a description, and why a test that fails when the page changes is doing its job.]]></summary>
        <content type="html"><![CDATA[<p>Most documentation tooling checks how a page is written, not what it claims. Vale catches style. codespell catches typos. Lychee catches broken links. None of them check whether the page is still describing something true.</p>
<p>This site has a page that describes its own pipeline: it involves Vale, codespell, Lychee, and Doc Detective, each doing a specific job. I wanted to know if that description would still be accurate next month, not just whether it read well today. So I wrote a test for it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="treating-a-description-as-a-claim">Treating a description as a claim<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description#treating-a-description-as-a-claim" class="hash-link" aria-label="Direct link to Treating a description as a claim" title="Direct link to Treating a description as a claim" translate="no">​</a></h2>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/docs/pipeline">pipeline overview page</a> has a clean table naming each tool and what it does. Until this week, nothing checked whether that table stayed accurate. If I swapped Lychee for a different link checker and forgot to update the page, then nothing would tell me. The page would keep naming a tool that wasn't there anymore, misleading the reader.</p>
<p>So I wrote a Doc Detective test that treats the page as a claim instead of a description: load the page, confirm the heading is right, and confirm the body still names all four tools. If any of that stops being true, then the test fails.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-when-the-requirement-changes">What happens when the requirement changes<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description#what-happens-when-the-requirement-changes" class="hash-link" aria-label="Direct link to What happens when the requirement changes" title="Direct link to What happens when the requirement changes" translate="no">​</a></h2>
<p>If I add a fifth tool to the pipeline tomorrow, or swap Lychee for something else, the test doesn't update itself. The page changes, the test does not, and the test fails. That's not a flaw, that's the design. The failure is a forced checkpoint: a human has to look at the diff between what changed and what the test still expects, and decide whether to update the test to match the new reality.</p>
<p>This is the same shape as a requirements test in any other system. Nothing automatically keeps a test in sync with intent, because the test's whole job is to represent intent as it was last confirmed. If it auto-updated whenever the implementation changed, then it would stop being a test and start being a mirror, agreeing with whatever it's pointed at.</p>
<p>The tradeoff shows up at scale. Four strings hand-typed into a JSON file works fine for one table with four rows. It doesn't work for a documentation set with fifty pages each making a handful of claims, because now the claims live in two places, the page and the test, and keeping them in sync is manual labor that someone eventually skips.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-fix-it">How to fix it<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description#how-to-fix-it" class="hash-link" aria-label="Direct link to How to fix it" title="Direct link to How to fix it" translate="no">​</a></h2>
<p>The fix at that scale is to stop hand-typing assertions and generate them from the documentation itself. Doc Detective already has a feature for exactly this, called <a href="https://docs.doc-detective.com/docs/tests/detected-tests" target="_blank" rel="noopener noreferrer" class="">detected tests</a>. You define markup patterns in your config, things like "bold text generates a <code>find</code> action" or "a Markdown link generates a <code>checkLink</code> action," set <code>detectSteps</code> to <code>true</code>, and Doc Detective scans your docs and regenerates the test steps from the content on every run. The table on the page becomes the source of truth, not a JSON file sitting next to it.</p>
<p>Here's where it gets specific to this repo: the table on the rendered pipeline page isn't the only copy. The repo's <code>README.md</code> has its own version of the same table, written for anyone reading the code instead of the deployed site. If the README is the table I actually trust, the one a person edits first when a tool changes, then that's where detection should point. Mark up the tool names there as bold text, turn on detection, and the test would track the README automatically. Add a fifth tool, and the test picks it up the next time it runs, no edit to <code>sample.json</code> required.</p>
<p>But Doc Detective loads the rendered <code>pipeline.md</code> page, not the README. So detection only solves half the problem. I'd also need something that checks the pipeline page's table against the README's table, so the two don't quietly drift from each other the same way the page and the old hand-typed test could. Detection would fix the test-versus-page gap. It would not fix the README-versus-page gap, that's a second, separate sync problem, and it's the same shape as the first one: two descriptions of one fact, with nothing forcing them to agree.</p>
<p>I didn't build any of that here, because one table with four rows doesn't need it yet, and <code>sample.json</code> is currently the more honest reflection of how this specific test works for a reader trying to follow along. But it's the real answer to "how does this scale": not a generator I'd have to build, but a feature Doc Detective already ships, plus a second check I'd still need to write, because automating one sync point doesn't automatically close the others.</p>
<p>At that point, I'd also want something downstream of detection that opens a pull request when a detected test changes shape, so a tool swap shows up as a reviewable diff instead of a silent regeneration. That's out of scope for this test, but it's the natural next step.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="making-the-result-visible">Making the result visible<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description#making-the-result-visible" class="hash-link" aria-label="Direct link to Making the result visible" title="Direct link to Making the result visible" translate="no">​</a></h2>
<p>The <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/doodle-test">doodle test</a> I wrote earlier updates two images on a schedule. It's a proof of concept for content automation: let the test keep something current so a person doesn't have to.</p>
<p>This test is a proof of concept for something else. It's not about keeping content current, it's about catching the moment content stops being current. The two are different problems, and conflating them is how a lot of stale documentation happens.</p>
<p>I also didn't want the result just sitting invisible inside a GitHub Actions log. So the workflow now writes a small status file with a pass/fail and a timestamp, and a <a class="" href="https://oddlittlebird.github.io/showcase-site/doc-detective/pipeline-test">new page</a> reads it and shows a badge. Green or red, right there, next to an explanation of what's being checked and why.</p>
<p>CI results that only live in CI tend to get ignored until something breaks loudly. Putting the result on the page it's protecting makes the check part of the documentation, not a separate system nobody looks at.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-actual-lesson">The actual lesson<a href="https://oddlittlebird.github.io/showcase-site/blog/testing-the-pipeline-description#the-actual-lesson" class="hash-link" aria-label="Direct link to The actual lesson" title="Direct link to The actual lesson" translate="no">​</a></h2>
<p>Style checkers and spell checkers verify that text is well-formed. Link checkers verify that references resolve. None of that tells you whether a sentence is still describing reality.</p>
<p>That's a different kind of test, and it's worth treating as its own category: not "does this page read well," but "does this page still tell the truth." The pipeline page now runs that check on itself, every day.</p>
<p>And if it is lying, then it says so right on the page as well as in an email to the overseer of its repo (me).</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Doc Detective dominos]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos"/>
        <updated>2026-06-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[My first Doc Detective test failed twice: once because the assistant used an outdated version, and once because I wrote the instructions for a human instead of a testing agent.]]></summary>
        <content type="html"><![CDATA[<p>Sometimes you start one thing, and it tips into another thing, and then another and another.</p>
<p>My first Doc Detective test for this site was like that. And honestly, I am glad. If it had gone smoothly, then I wouldn't have learned nearly as much.</p>
<!-- -->
<p>My concept was pretty simple: Create a test that shows how Doc Detective can update screenshots.</p>
<p>It seemed like the Google doodle would be an obvious and easy use case.</p>
<ul>
<li class="">It's public.</li>
<li class="">It updates every day.</li>
<li class="">Everyone knows about it.</li>
</ul>
<p>So I wrote step-by-step instructions, took a screenshot and downloaded the doodle GIF, and then told my robot minion (Claude Code) to help me write the tests.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mistakes">Mistakes<a href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos#mistakes" class="hash-link" aria-label="Direct link to Mistakes" title="Direct link to Mistakes" translate="no">​</a></h2>
<p>First mistake: I did not tell it to use the most recent version of Doc Detective. I'd heard this could be a problem with coding assistants; that if you do not explicitly instruct them to use the latest, then they might not. In this case, it didn't.</p>
<p>I realized the issue when I ran Doc Detective on the tests locally and it failed. So I had the minion update the tests, tested again, and was successful.</p>
<p>Second mistake: I did not understand who I was writing instructions for. I wrote instructions for humans. The testing agent is not a human.</p>
<p>I had instructions like, "Navigate to google.com" and "Observe the doodle," as one would for a human. That doesn't work for the bot.</p>
<p>If I hadn't had Manny Silva demo the functionality for me a while back, then I wouldn't have thought to ask Claude Code why it was writing tests in a separate file rather than inline, as I had seen in the demo.</p>
<p>Turns out that when you write for a human, the robot has trouble parsing instructions. They just aren't specific enough for it to use the human-readable steps as a basis for tests. As a result, the tests were written to a separate file.</p>
<p>And that, for the record, is totally valid. For this use case, I wanted to try the in-line tests. Claude Code helped me rewrite the instructions so that the testing agent would understand them, which meant we didn't need a separate testing file.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="workflow-updates">Workflow updates<a href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos#workflow-updates" class="hash-link" aria-label="Direct link to Workflow updates" title="Direct link to Workflow updates" translate="no">​</a></h2>
<p>After all of that, I ran my regular workflow. Vale, bless its little heart, dutifully complained that I hadn't spelled out my acronyms: GIF and PNG.</p>
<p>So since I don't want it to bug me every single time I use those acronyms, I had Claude update Vale to not apply the rule to GIF, PDF, PNG, or JPG.</p>
<p>And then it turned out I somehow hadn't installed Vale or codespell locally, only on the remote repository, so I had to install those and told Claude to make sure the local and remote Vale configs stayed the same.</p>
<p>Once all of that was finished and merged...it didn't work. So I began to troubleshoot.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="troubleshooting-and-fixing">Troubleshooting and fixing<a href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos#troubleshooting-and-fixing" class="hash-link" aria-label="Direct link to Troubleshooting and fixing" title="Direct link to Troubleshooting and fixing" translate="no">​</a></h2>
<p>Here's a partial list of things that went wrong before it worked:</p>
<ul>
<li class="">The schema I was writing tests against was two major versions out of date</li>
<li class="">A <code>sharp</code>/<code>libvips</code> dependency was silently missing from global npm installs in CI, causing cryptic <code>ERR_DLOPEN_FAILED</code> errors across five separate debugging sessions</li>
<li class="">The <code>GITHUB_TOKEN</code> that GitHub Actions uses can push commits, but those commits deliberately cannot trigger other workflows — a loop-prevention feature I had never had to think about before</li>
<li class="">The workflow was running at midnight Pacific time, before Google had actually updated the doodle for the day</li>
<li class="">Two workflow runs fired at the same time and one of them rejected the other's push</li>
</ul>
<p>Each one of these felt, in the moment, like a dead end.</p>
<p>What eventually worked: upgrading to the latest version of Doc Detective (which bundled its own sharp), adding a <code>workflow_run</code> trigger so the deploy fires after the doodle commit, and moving the schedule to 2 AM — which Google has reliably beaten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dominos">Dominos<a href="https://oddlittlebird.github.io/showcase-site/blog/doc-detective-dominos#dominos" class="hash-link" aria-label="Direct link to Dominos" title="Direct link to Dominos" translate="no">​</a></h2>
<p>How many dominos did I tip over in this workflow?</p>
<p>Let's see...</p>
<ul>
<li class="">Told Claude Code to always use the most recent version of libraries and tools.</li>
<li class="">Installed a couple tools that I missed installing locally.</li>
<li class="">Updated Vale configs. Again.</li>
</ul>
<p>Mind you, this workflow is still pretty immature. As I continue to iterate and improve the site, I expect that there will either be fewer workflow dominos that fall over with each new change, or at least, they will be new and different ones.</p>
<p>The bigger lesson? Automation doesn't save you from debugging. It just means the debugging happens at scale, in CI logs, instead of on your own machine.</p>
<p>But when it works? The page updates itself while I sleep. That still feels like magic.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Home page woes]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/home-page-woes</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/home-page-woes"/>
        <updated>2026-06-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Converting the home page to MDX so Vale would lint it added a sidebar I never asked for, a small lesson in supervising an AI-assisted workflow.]]></summary>
        <content type="html"><![CDATA[<p>Today I had an object lesson in why I need to supervise my workflow.</p>
<p>I've been making updates around the site, as one does, and realized that Vale wasn't running on my home page or portfolio site. I inquired of Claude Code as to why, and it said that those pages were .tsx files, which Vale ignores.</p>
<p>Mind you, I hadn't asked it to make them .tsx files. It just did it. So I asked if there was any reason they shouldn't be .mdx files. It said there wasn't, so I had it change them to .mdx. That way Vale would review them.</p>
<p>No biggie, right?</p>
<p>Well, today I checked the home page again, and lo and behold, there was a new sidebar on it. Grr.</p>
<!-- -->
<p><img decoding="async" loading="lazy" alt="Top of home page with sidebar" src="https://oddlittlebird.github.io/showcase-site/assets/images/blog-post-3-c72e829750930bb91ebd6d462e27554e.png" width="1626" height="593" class="img_ev3q"></p>
<p>Turns out that when Claude Code changed the home page to .mdx, Docusaurus did its thing and added a sidebar.</p>
<p>Now, a human knows at a glance that is wrong. My robot minion, of course, did not.</p>
<p>So I had it change the home page back to .tsx and all was well.</p>
<p>The moral of this story, and the point of the exercise of building and using an AI-assisted workflow, is to find these stumbling blocks and identify them.</p>
<p>Right now, this POC site is about the smallest use case you could realistically imagine for a production system. It's dead simple: a portfolio site plus a blog. Small things compound — and the earlier you find them, the easier (and cheaper) they are to fix.</p>
<p>Now take this site and multiply it exponentially, pages and problems, to extrapolate what an enterprise production system will look like. How much planning, training, and supervision will that take?</p>
<p>Scary, no?</p>
<p>This is why we practice. This is why we test. This is why we iterate.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Website" term="Website"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Learning Git lessons]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/learning-git-lessons</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/learning-git-lessons"/>
        <updated>2026-06-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What changed when I went from working in someone else's repo to owning my own, including the failed workflows nobody else was going to fix.]]></summary>
        <content type="html"><![CDATA[<p>Being a GitHub repo owner is a trip, I must say.</p>
<p>Like many of us, I have worked in Git for years. YEARS.</p>
<p>I've managed GitHub teams, reviews, branches, fixed merge conflicts, and so many other tasks.</p>
<p>But none of those repos were <em>mine</em>.</p>
<p>Until now.</p>
<!-- -->
<p>As a writer working for a company, I always had someone else to handle all that setup stuff. I told the engineers or ops team what I needed, and they made it happen. Automagically.</p>
<p>But this repo is, while not the first repository I have ever owned, definitely the first that I have seriously managed.</p>
<p>Now <em>I</em> am the one who gets the notifications that a workflow has failed and need to deal with them. I don't have a team to forward them to, it's just me and my robot minion (Claude Code).</p>
<p>Honestly, it's empowering in many ways. Instead of Kermit flailing helplessly at the error messages, I can work with the assistant to figure out where the workflow is broken and how to fix it.</p>
<p>If you haven't done something like this before, it's an experience I highly recommend.</p>
<p>For example, when I merged my previous blog post, my workflow kicked off with lychee, a link checker, throwing a bunch of errors. I had to track down the right logs, feed them into Claude Code, and work through the fix with it.</p>
<p>Every problem that I encounter is a learning opportunity, and I love it.</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
        <category label="Workflow" term="Workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[It begins!]]></title>
        <id>https://oddlittlebird.github.io/showcase-site/blog/it-begins</id>
        <link href="https://oddlittlebird.github.io/showcase-site/blog/it-begins"/>
        <updated>2026-05-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why I built a public showcase site with a running documentation pipeline, instead of relying on work samples locked behind NDAs.]]></summary>
        <content type="html"><![CDATA[<p>This site is launched!</p>
<p>I mean, it's been online for a while, but I feel like this blog post is what makes it complete.</p>
<p>You see, this post is what marks the beginning of the workflow testing.</p>
<p>Let me explain.</p>
<!-- -->
<p>A while ago, I realized that we documentarians need a way to demonstrate what we do, to really show our value. Unfortunately, a lot of our best stuff is frequently in company hands and hidden behind layers of NDAs. My solution?</p>
<p>Build it ourselves.</p>
<p>Hence, this showcase site was born. It's a proof of concept (POC) site that not only has the information about me that a hiring manager might want (resume, samples, and so on), it also has a GitHub repo showing a semi-automated documentation workflow.</p>
<p>GitHub actions, Doc Detective, agent skills, Vale, and so on. They're all here, and they will run on every page that I write and merge. This will allow me to test and improve the workflow over time.</p>
<p>And because the repo is public, potential employers can see the backend as well as the frontend.</p>
<p>Future blog posts will discuss more about the process and lessons I learn. But for now...launch!</p>]]></content>
        <author>
            <name>Diana Payton</name>
            <uri>https://www.linkedin.com/in/dianapayton/</uri>
        </author>
    </entry>
</feed>