vibeapp.win

What broke

Centiliters read as milliliters linked bottles to the wrong size

Our wine app read Vinmonopolet's storefront volume as milliliters when it is given in centiliters, so 75 cl became 75 ml, and the matcher ignored volume anyway. A standard 0.75 L bottle could get linked to a magnum or a half bottle with the same name. We fixed it on 2026-06-13 and made three rules from it.

The short version

What went wrongVinmonopolet volume in centiliters was parsed as milliliters (75 cl as 75 ml) and the matcher ignored volume, so a 0.75 L bottle could link to a magnum or half bottle.

How we found itThe fix commit noted the search result had a volume field that was unused and misread, and the price refresh had two more wrong-link paths.

The rule nowUse every field to tell candidates apart and check its unit, set a confidence floor before linking, and don't fall back to name search when an existing link can't be fetched.

This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes

One app in the park keeps track of a wine cellar. It matches bottles to products at Vinmonopolet (the Norwegian state wine retailer), links a bottle to its product and refreshes the price. A fix committed on 2026-06-13 dealt with a fault where this link could point to the wrong bottle size. A standard 0.75 L bottle could end up linked to the 1.5 L magnum or to the half bottle of the same wine, and then it showed that bottle’s price.

What went wrong

There were two faults, and they made each other worse.

First, Vinmonopolet’s public API gives no EAN or GTIN. A later commit that same afternoon confirmed this: the API does not return barcodes, and searching for one gives zero hits. So the app cannot tell variants apart by barcode. It has to match on name. Vinmonopolet lists a magnum and a standard bottle as separate products under the same name, so a name search often brings back several sizes of the same wine.

Second, the search results do say what volume each product has, and we were not using that. The fix commit says so plainly: the search result has a volume for each product, in centiliters. We did not use it, and on top of that we parsed it wrong, so 75 cl became 75 ml.

The old parser guessed the unit from the size of the number. Values under 10 were treated as liters and multiplied by 1000. Anything else was taken as milliliters. The storefront gives volume in centiliters; the code comment added in the fix uses the example of a value of 75 with the label “75 centiliter”, which means 750 ml. A value like that fell into the “already milliliters” branch and was stored as 75 ml. The volume was wrong, and the matcher did not use volume anyway. The commit sums up the result:

“en 0,75 L-flaske kunne lenkes til magnum (1,5 L) eller halvflaske med samme navn.”

(A 0.75 L bottle could be linked to the magnum (1.5 L) or a half bottle with the same name.)

How we found it

The search result already had the field that would have settled it, and that field was both ignored and misread. The matcher ranked candidates by name similarity alone. When the same wine came back in two sizes, whichever catalog name looked a little closer to the query won. The code comment added in the fix notes that the correct size often has a longer catalog name, with extra appellation or vintage text, so it can score lower on name than the magnum.

Looking at the price refresh turned up two more ways it could link to the wrong wine:

  • When a bottle had no link yet, the refresh did a name search and took the top result, however weak the match. The commit gives an example: a loose search for “Tête de Cuvée” could link a wine from a different producer.
  • When a bottle was already linked but that product could not be fetched (for example because it had been delisted), the refresh fell back to a name search. In the commit’s words, that “festet feil vins pris til flasken”: it attached the wrong wine’s price to the bottle.

The fix

All of it went into one commit on 2026-06-13:

  • Volume parsing. The parser now reads the unit from the readable label: milli, centi or liter. When there is no hint, it assumes centiliters, which is the storefront’s norm. 75 cl becomes 750 ml.
  • Volume in matching. The volume of the scanned bottle is now passed through to the search, with 750 ml as the default. When candidates are re-ranked, an exact volume match adds 25 points to the name-similarity score. The comment in the code explains the number. In the cases we saw, the gap between the right size and a competing size was about 20 points, and clearly unrelated wines were about 40 or more apart. A 25-point bonus lets the right size win without promoting a different wine. If two candidates still tie, the one closest in volume wins.
  • Confidence floor. The price refresh now only adopts a new link if the name match clears the same floor the scan path already used. If it doesn’t, it logs a low-confidence skip and leaves the bottle unlinked. The commit’s reasoning is “better unlinked than wrong.”
  • No fallback for existing links. If a linked product cannot be fetched, the refresh records that it checked, keeps the link and skips the cycle. It no longer runs a name search.
  • Tests. A test for the volume tiebreak (the standard bottle beats the magnum when the names are identical).

A follow-up the same day

About forty minutes later, a second commit showed that “cannot be fetched” did not always mean “delisted”. Vinmonopolet’s product detail endpoint returned HTTP 400 for some products that were active and could be bought. With the new no-fallback rule, the refresh would have kept skipping those bottles. The fix fetches the product through the search endpoint using its exact product code. That is a lookup by code, not a guess by name, so it does not break the rule.

The same commit records the limits of the approach: name matching is inherently imprecise, and can both miss the right wine and hit the wrong one. That is why the confidence floor exists, and why manual linking stays available as a backstop.

Who made the mistake

The commit message says “vi” (“we”), so we don’t attribute the original parser or the matcher to anyone here.

The rule

  • Use every field you have to tell candidates apart. If the API has no barcode, use volume, vintage and producer, and check what unit each field is in before trusting it.
  • Set a confidence floor before adopting a new link. An unlinked bottle is better than a wrongly linked one.
  • Don’t fall back to a name search when an existing link can’t be fetched. Keep the link, skip the cycle, and if needed look the product up again by its exact code.