← Jon Loome
What it is
Shelf lets a shop owner manage their Shopify catalogue by typing what they want. Rewrite the Rega Planar 3. Put the Entreq Olympus range to draft on Friday. What's failing an SEO check? Shelf drafts it, shows a preview, and does nothing until the merchant approves. Every write is reversible.
I run ecommerce and marketing for an independent hi-fi shop in Norwich, with a catalogue of about 1,150 products. The work Shelf does is work I was doing by hand. It has been developed against that live store throughout — real products, real customers, real damage when it got things wrong.
I am not a trained software engineer. What I have is ten years of running the catalogue this application writes to, and AI coding tools let me turn that into working software. For an application of this kind that is the relevant fact rather than an embarrassing one: the interesting question is not whether a model can produce code, it is whether the person directing it can tell when the output is quietly wrong. Most of what follows is about that.
By the numbers
- Server code
- 16,858 lines JS
- Test cases
- 662 0 failing
- Catalogue
- 1,153 products
- SEO rules
- 23
- Models
- 2 Sonnet + Haiku
The thesis
Every serious bug in five months was an ordinary systems bug wearing an AI costume.
Not one of them was fixed by changing a prompt. An index misaligned in a batch loop. A tokeniser blind to a forward slash. A fallback that returned the whole candidate list when it recognised nothing. A language model handed the last word on whether it had actually done the thing it said it had done.
The prompt file already carried five escalating instructions never to invent a specification, and it invented them anyway. That is the lesson I would bring to any team building on top of a model: the prompt is where you express intent, and it is almost never where you enforce it. Enforcement is code — deterministic, testable, and cheap.
Four incidents make the case. All four are real, all four are in the git history, and two of them damaged live customer-facing data before I caught them.
Four incidents
SHF-BH35
11 Aug 2026
server/routing.js
One subwoofer's price, applied to 250 listings
A merchant typed change price of the rel ht1003 mk2. Shelf came back with a confirmation covering 250 products — every REL subwoofer in the shop, plus assorted Rega and Naim. It was discarded before writing, so nothing was damaged, but it needed two independent faults to line up.
The first was punctuation. The real title is Rel HT/1003 Mk2, and five separate tokenisers each carried their own copy of a regex that folded - and . inside a model number but not /. The search token matched no title at all — not the target, not even its siblings.
The second was the dangerous one. With every narrowing level having missed, the target selector ended by returning the full candidate list, and Shopify's loose search had handed it 250 rows. "I recognise nothing here" and "I mean all 250 of these" were the same return value on a write path.
return products;
→
return []; // caller reports not-found, names the 5 closest, queues nothing
Batch off-by-one
10 Jul 2026
batch parser
183 products, each holding the previous product's description
The batch publish path zipped drafts against products by array index, and the arrays drifted apart. Every product received the description belonging to the one before it. I found it by following the chain: the Denon AVC-X3900H page held the AVR-X2900H's copy, the AVC-A1H page held the X3900H's, and so on through Gold Note, KEF, Fyne, dCS and Bowers & Wilkins.
183 products across 27 brands went live wrong. All were restored from the undo log — which existed because reversibility had been a requirement from the start, and is the single design decision that saved this project.
What matters about this one is what it isn't. It looks exactly like a model inventing content, and every instinct says to go and harden the writing rules. It was arithmetic. No amount of prompt engineering would have touched it.
drafts zipped to products by index
→
parsed per record + identity gate on every drafted title
SHF-CSQ5
16 Jul 2026
server/routing.js
It said "Done" six times and had written nothing
Shelf reported clearing a set of compare-at prices. It had not cleared them, and could not have — the code could set that field but never unset it, so every "cleared" it had ever said was false. I passed the claim on to my employer before checking. That is the bug I would put first in any brief about deploying these systems.
The cause was structural. When the intent classifier misses, the request falls through to a chat model, and a chat model cannot act but will happily describe acting. Two guards existed to catch an empty action queue, and both were keyed to the word "publish" — while Shelf's actual wording is "Say yes to confirm". They never fired.
The fix moves the authority: code now owns all success wording. Free-form model text that claims a completed change is replaced and logged unless a real store write was recorded on that request. The compare-at feature was removed entirely rather than left half-working.
model writes "Done."
→
claimsCompletedWrite() intercepts unless _didStoreWrite
SHF-HRAX
7 Aug 2026
server/index.js
A listing built from a different product's specification
A spec sheet for the Naim Uniti Star Lite was uploaded with an instruction to create the product. Shelf ignored the uploaded sheet, went to the web, found the Naim Uniti Core — a different machine — and published 24 figures that belonged to it.
Five faults, and the first is the one worth repeating. A query that matched nothing fell back to products[0], Shopify's top full-text hit. The matcher had done its job correctly and returned matchedModel: false; the calling code simply didn't look at it. A correct signal that nobody reads is not a safeguard.
Alongside it: the uploaded document never reached the verification guard, and a total mismatch between draft and source only logged a warning instead of blocking. Product routing is now decided by a catalogue lookup that returns rewrite, write, or ask — and ambiguity always asks.
fall back to products[0]
→
name the near-misses and ask which one
The common thread
Three of the four are the same shape: a fallback that guesses when it should stop. On a read path a wrong guess is a bad answer. On a write path it is damage to a live shop, and the two need entirely different defaults. Uncertainty has to be a first-class return value that the caller is forced to handle — not an empty result quietly standing in for "everything".
Making it cheap
A clean product rewrite originally made six model calls and cost roughly £0.20–0.30. Two brand ranges came to about $20. The obvious lever was the specification handling, which was also the accuracy problem — the model was writing figures, then three further calls were spent asking it to check its own figures.
Both problems had one answer: stop the model writing specifications at all. Code lifts the manufacturer's specification block verbatim and pastes it in. The model writes prose only, and prose is forbidden any figures. A deterministic scan extracts every number-and-unit from the draft and strips any that isn't character-for-character in the source. No model call, no cost, and — unlike an instruction — it cannot be ignored. Four of the six calls disappeared.
I built the cost accounting first, so the saving could be measured rather than assumed: an async-local bucket per HTTP request, the Anthropic client wrapped once so no call site escapes it, cache reads and writes priced separately. Web search bills per search rather than per token, so that rate is read from an environment variable and never guessed — searches are still counted, so the count stays trustworthy even when the rate isn't set.
Separately, the system prompt was split into three cached blocks, with the large writing-rules block sent only when the request is actually a writing request. Non-writing calls — price checks, stock questions, ordinary conversation — dropped from about 5,500 tokens to about 1,750.
What I test, and why
662 cases across 30 files, green as of today. Three of the rules behind them were bought with damage.
-
01
Skipped is never passed. The SEO ruleset reports a check it couldn't run as SKIPPED and excludes it from the score denominator. The catalogue sweep doesn't fetch images, and silently passing the image rules would have rebuilt exactly the false all-clear the ruleset was written to remove.
-
02
A test file CI doesn't run is not a test. The regressions that let SHF-BH35 through were sitting in a file the npm test script never invoked. The suite was green and the suite meant nothing. Worse, an existing test named "no match falls back to full fuzzy list" never exercised the no-match branch at all — its fixture matched both products.
-
03
When a matcher gets more permissive, the negative test is the one that matters. The first cut of the BH35 fix introduced two new bugs by widening the match: "Rega Planar 3" began sweeping "Rega Planar 3/24V". Every test I wrote first time asked whether it found the right product. None asked whether it now also found the wrong one.
On top of the unit tests there is a replay harness built from real failures — each named for the incident it came from, run against the actual handler rather than a mock. A separate pipeline audit runs a corpus of real products end to end before a release, so the long tail surfaces in one batch instead of one live failure a day. Its first free run found five manufacturer sites that block automated fetches — not bugs, but the difference between Shelf saying "I couldn't find specs" and "that site blocks us, please paste them".
What isn't done
The version of this document that claimed otherwise would be less useful to anyone reading it.
Billing is not live
Tiers are built and priced but the environment configuration currently forces every charge into test mode. It is the last blocker before launch.
Google verification pending
Search Console is connected and reading live data. Analytics was dropped in October 2026 because nothing on screen used it, and Google approves only the permissions an app visibly needs. Until verification passes, the Google connection stays closed to other shops.
No onboarding flow
A first-run conversation to capture shop voice and details hasn't been built, and the app is not yet embedded via App Bridge — likely required for App Store approval.
973 of 1,153 failing
The first full ruleset run found most of the catalogue failing at least one check. At ten pages a day that queue is about three months long. Shelf works; the shop is still behind.
Where this leaves me
I set out to solve a specific problem in my own shop and ended up with a reasonable education in why systems built on language models fail in production. The failures were not exotic. They were fallbacks, off-by-ones, unread return values and a component trusted to report on itself — the same faults as any other software, made harder to spot because the output is fluent prose that reads like success.
What I would want to be judged on is not the code volume. It is that every incident above was found, traced to an actual root cause rather than a symptom, fixed, tested against the case that would have caught it, and written down.
Jonathan Loome
jonloome@gmail.com
Norwich, UK
Figures verified against the repository, 8 September 2026