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

The chart that would have said nothing

Build LogAugust 10, 2026 by Jeremy Schoemaker

2026-08-10

Two design requests today. Both were reasonable, both were specific, and both — measured before a line of code got written — turned out to describe something that could not carry information.

The first was "use a percentile system." The second was "merge these two redundant columns." Building either as asked would have shipped a confident-looking panel that said nothing, and in one case would have silently deleted seven rows of real data.

Neither measurement took longer than a minute. That ratio is the whole point of this post.

"I would rather use a percentile system"

The ledger's four columns each printed a raw count next to every row — 12,481, 9,930, 8,802. The ask was to drop the numbers and draw a bar instead, filled by percentile.

A percentile is a real statistic. The columns hold real counts. Nothing about the request is naive.

But every one of those columns is built by a query ending in LIMIT 10, off a table of 1,431 phrases and 650,000+ citation rows. Which means every row visible on the page is, by construction, in roughly the 99th percentile of its population. Draw a percentile bar and you get forty bars — four columns, ten rows each — rendered identically full. A chart with zero variance, occupying the most valuable real estate on the page, looking authoritative.

The selection had already done the ranking. The percentile had nothing left to say.

What varies inside a top ten is each row against the top of that ten. So the bars became share of the column leader: the first row is full by construction, the rest are honest relative magnitudes, and the exact count moved into the hover title so nothing left the record. The code comment says, in as many words, not a percentile, and why — because the next person to read share / max will otherwise "fix" it back.

One thing carried over from the original request unchanged: the fill is quantized to ten steps. A continuously-drawn width invites reading a three-pixel difference between two rows as a finding. Ten steps say magnitude, not measurement.

"There is a lot of redundant stuff in these two columns"

The page carried two advertising panels side by side: Top Advertised Phrases and what they link to, and Top Advertised Domains. The observation was that they overlapped and should be one thing.

Correct — and worse than it looked. Ten phrase-ranked rows resolved to three distinct destinations:

107 Best password manager for personal use -> 1password.com 98 Best headphones for running and swimming -> shokz.com 98 Best coffee maker with k cup combo -> www.ninjakitchen.com 98 Best password manager for individuals -> 1password.com 95 Best password manager 2026 -> 1password.com 94 What is the best password manager for a... -> 1password.com 90 Best coffee maker for iced coffee -> www.ninjakitchen.com 83 Best espresso machine ninja -> www.ninjakitchen.com 82 Best password manager for families 2026 -> 1password.com 82 Best password manager -> 1password.com

Six rows restating 1password.com. The panel was mostly one domain wearing ten hats.

So the obvious merge is phrases carrying their destination — one row per phrase, domain attached. That was the direction I was about to build. Before doing it, I diffed the two lists:

unmatched domains (would vanish if merged onto phrases): ['www.namecheap.com', 'www.notion.com', 'www.chevrolet.com', 'www.caranddriver.com', 'www.nespresso.com', 'oralb.com', 'www.hostinger.com']

Seven of ten advertised domains reach no phrase in the phrase top ten at all. The obvious merge direction would have deleted them from the page with no error, no warning, and a result that looked tidier than what it replaced.

Inverting it fixed both problems at once. Rank by domain, hang each domain's top advertised phrase off it: the 1password repetition collapses to one row, and every domain survives. It cost one extra grouped query in the controller — and removed three, because the phrase-ranked query it replaced could then be deleted outright.

The generalizable rule, now written into a skill: before merging two ranked lists, print the orphans on each side. Whichever side has them must drive the merged ranking. If both sides have orphans, the lists are answering different questions and should not merge at all.

Three things that looked fine and were not

A class that was never defined. .num-badge was referenced by three components. It appears in no stylesheet, anywhere in the repo. CSS has no concept of an undefined class, so it rendered as plain inherited text and had done for as long as it existed. It survived review precisely because a plain element does not look broken — it looks unremarkable. Two of the three components using it got deleted today without anyone noticing the class did nothing.

A background that was already white. The request was to make a panel's background white. The token was already #ffffff. The visible tint came from two position: absolute; inset: 0 texture overlays spanning the entire shell — and a static child paints underneath a positioned sibling regardless of source order. The fix was not a colour at all; it was position: relative plus its own fill. Changing the colour token would have done nothing, twice.

A total outage that was not. Mid-session the homepage started returning 500 after a 30-second timeout. The error pointed at Connector.php:67 — which is the connect phase, not a query. Not a slow database: an unreachable one. ipconfig getifaddr en0 returned 172.20.10.9, an iPhone hotspot subnet, so the entire 192.168.1.0/24 LAN was dark. Nothing to do with any change made today. Back on the LAN, the same page went from a 30-second timeout to 200 in 0.45s.

I nearly spent an hour on that one before checking which network the laptop was on.

The one I did to myself

Late in the session I ran git add -A resources to commit a bar-styling change. Sitting in that directory was a half-finished edit to stability.js — the first step of a redesign of a different page entirely, whose remaining steps had not been written yet.

It went in. The commit message talks exclusively about bars. The phrase detail pages are now live with their colour ramp removed and none of the redesign that was supposed to accompany it — a half-converted state, shipped under a message that gives no hint it happened.

This is the second time today a git add -A did this. Earlier, a different concurrent session ran git commit -a for its documentation work and swept up four of my open UI files under the message "docs: sync docs to reality." I went to commit and found a clean tree — not because the edit failed, but because it was already committed inside somebody else's change. Already pushed, so it stays.

Scoping the add to a directory is not protection. The scope was resources, and the unfinished file lived in resources.

What this day was actually about

Nothing shipped today that was hard to build. The header change was three lines. The bars are a div with a width. The merge was one query.

What the day was actually about is that two of the requests, taken literally, described charts that could not carry information — and the only reason that got caught is that both were cheap to check first. A one-minute script against the live page props was the difference between a panel that ranks ten domains and a panel that silently ranks three.

The expensive version of today is the one where both got built as asked, looked fine, and had to be discovered later by someone wondering why every bar is full.

← Back to blog