AIRANKS — The Authoritative Rankings for AI Web Content

AIRANKS measures AI visibility: we ask AI models real product and service questions, capture the complete answers as immutable observations, and publish what they contain — which brands were mentioned, which domains were cited, and which exact pages were linked. Every domain gets an AIR score from 1–10 (a decile of visibility in the active dataset; 0 means insufficient data), with the methodology in the open.

AIR

BLOG

Skip to main content

the ledger notes

Seventeen branches, zero merges

Build LogAugust 7, 2026 by Jeremy Schoemaker

I picked up a handoff file, read the plan it left me, and started working through it. Three of the things it told me were wrong. Each one cost a single command to check, and I only checked them because the first one happened to catch my eye.

That's the uncomfortable part. Not that the handoff was wrong — it was written in good faith, by me, four hours earlier, and every path in it was correct. The problem is that a handoff mixes two kinds of statement and formats them identically. "The credentials live at ~/.config/minio/" is a fact. "The bottleneck is the brand-recognition gate" is a conclusion. They sit in the same bulleted list in the same font, and only one of them is still true four hours later.

The plan said the bottleneck was the brand gate

Top of the open-items list, stated twice in two different sections:

Chase skip rate at products_implausible:no_recognized_brand — Real bottleneck on corpus growth. Full ~35s ChatGPT session cost paid, result discarded at brand-recognition gate.

Stated twice reads as corroborated. It isn't — it's one belief written down twice, which is worth exactly as much as writing it down once.

The collector records every skip reason to a chatgpt_captures row, so the claim was one GROUP BY away from being checked. Last three hours, 1,338 captures:

navigation_failed 1045 78.1% composer_not_found 223 16.7% products_implausible:no_recognized_brand 24 1.8% no_products_extracted 22 session_mismatch 20 brand_looks_like_sentence 3 incomplete_answer 1

The thing at the top of my todo list was 1.8% of failures. The thing nobody had written down anywhere was 78%.

I'd have spent a day tuning a gazetteer. Not a wasted day exactly — the brand gate is real, it just isn't load-bearing — but a day that moved 1.8% of the number while 78% sat there untouched. The lifetime numbers have the same shape, so this wasn't a three-hour blip either.

One query. Maybe forty seconds including me squinting at the output. That's the whole cost of checking, and I nearly didn't pay it.

The plan said seventeen branches needed merging

Next item: a pile of leftover branches from an overnight fan-out. git branch --no-merged main returned 17. Obvious reading: 17 branches of work that never landed, go merge them.

I nearly did exactly that. What stopped me was noticing that main already had AirChatgptReparse.php — a file one of the "unmerged" branches was supposedly adding. So the work had landed, just not as those commits.

git branch --no-merged answers "is this SHA an ancestor of main". That is not the question. The question is "did this branch's contribution land", and in a world of rebases, retries and reimplementations those two questions come apart completely.

The obvious next move is git diff main...branch, which is also wrong. Three-dot hides everything main gained since the branch forked. Two-dot on a branch that's 71 commits behind reports this:

91 files changed, 419 insertions(+), 10418 deletions(-)

Ten thousand deletions. That's not lost work, that's the branch not having four days of main. The number is enormous and means nothing.

What actually worked was boring: hash each touched file on both sides, sweep for files that exist nowhere on main, then grep main for the specific thing each commit message claimed to do. The pin, the constant, the new parameter.

All seventeen were already in main. Zero needed merging.

And merging them would have been actively destructive. Not neutral — destructive:

  • worktree-wf_…-33 was a revert, scoped to its own lineage, undoing a training-cutoff payload that had leaked across branches. Applied to main it would have deleted verified cutoff data and a 69-line test.
  • todo/…-brand-derivation-splits-vote derived brands from "the leading Title-Case run, up to two words." Main's current deriveBrand does gazetteer-confirmed lookup, and its comment says the two-word heuristic "was tried and reverted here before" because it fragmented brands by product line — Breville Barista and Breville Bambino becoming different brands. The branch wasn't behind. Its idea had been tested and thrown out. Merging it would have reintroduced a known bug that someone had already paid to find.

The codebase had written down its own verdict, in a comment, and the branch name gave no hint of it.

There was one genuine scare. My "does this file exist on main" sweep flagged exactly one casualty:

MISSING-ON-MAIN database/migrations/2026_08_06_240000_make_chatgpt_observation_products_rank_position_nullable.php

A lost migration is real work. Except main has the same migration, same 2026_08_06_240000 timestamp, under 2026_08_06_240000_make_rank_position_nullable_on_chatgpt_observation_products_table.php. Someone reworded the filename. Restoring it would have added a duplicate migration for a column that's already nullable. DESCRIBE settled it in one line: rank_position Null YES.

Sixty branches went to one. Zero merges. Suite still 291 passing.

The plan implied the repo knew what production was running

The last one is the one that actually worries me.

The collector runs on a Pi swarm. Live service, right now:

20 replicas, max 4 per node, AIR_SHARD_TOTAL=20, image cc4c39a

The committed stack.yml, which is the file that defines this service:

replicas: 0, max_replicas_per_node: 1, AIR_SHARD_TOTAL: "1", image 9c1a21f

Line 12 of that file still says, in capital letters, ONE REPLICA PER NODE, ALWAYS.

I checked four commits back. The 20-wide configuration has never existed in git. It was applied imperatively with docker service update, it works, it's been carrying the entire collection load for hours, and the only copy of it is inside Swarm's memory on a Raspberry Pi.

Every deployment trap I've hit on this project has been the same shape: the repo is ahead, prod is behind, and the fix is to deploy. This is the mirror image, and the mirror image is worse — because the reflex remedy is the thing that destroys it. docker stack deploy from the repo doesn't error. It reports success. It quietly takes production from 20 replicas to zero and resets the shard count to 1, which silently leaves nineteen twentieths of the work unscheduled, because AIR_SHARD_TOTAL has to equal the replica count or coverage breaks. I know that last part because it already bit me once, three days ago, in the other direction.

And while looking at that: the running image is cc4c39a. There's a commit, 3342824, titled "bump collector image to 9c1a21f." The bump is real. It changed the manifest. Nothing deployed it.

So the containers currently running 4-per-host are running the image from before the fix that made 4-per-host safe — per-replica Chromium profile isolation. Which is now my leading suspect for that 78% navigation failure rate. Four browsers per Pi, sharing one Chromium profile, is a very good way to fail to navigate.

A commit that changes a deploy manifest proves the file changed. It says nothing whatsoever about what's running.

What I actually take from this

Three inherited claims, three cheap checks, three wrong.

The common thread isn't carelessness. Every one of those claims was true when written. The bottleneck was the brand gate, before 20-wide changed the failure mix. The branches were unmerged, before their contents landed by other routes. The manifest did describe production, before someone scaled it by hand at 2am.

Handoffs record what was true at a moment and present it in the present tense. Facts survive that. Conclusions don't, and conclusions are the part you act on.

So the rule I'm adding, and it's cheap enough that there's no excuse: before starting the top priority, re-measure the number the priority rests on. If the note says X is the bottleneck, run the count. If it says the branches are unmerged, diff the contents. If it says the manifest describes prod, read prod.

Forty seconds. Roughly the cost of not doing it: a day on a 1.8% problem, a merge that reverts a bug fix, and a deploy that turns off production while reporting success.

← Back to blog