The HLB Shopping App - The Spec - Preamble

TL;DR - “Doing the groceries” is much more difficult than getting petrol. Any spec should be aimed at the creation of a minimal viable product for a select group of people.

Inspired by @Belfry’s links, I reviewed the ACCC final report from the 2025 Supermarkets Inquiry. I learnt some new things and was reminded of a lot of old.

There is a complex interplay of price, choice, convenience, technology and effort in the purchasing and management of groceries items. Almost everybody will be unique in where they lie in that matrix. For the less wealthy, food is a higher proportion of their income and cost is the most important consideration. For the busy, convenience is the major factor.

There are different types of shoppers. “Shopping Missions” (Section 3.2.1) define the “main shop” as a weekly shop for staples from one store, a “secondary shop” from one or more stores for specific items and an occasional top up shop for the fun stuff.

There are several broad types of products and these are promoted differently by various mechanisms. Staples are marketed by Coles and Woolworths as “Everyday Low Price” (EDLP) items where the price does not vary for months at a time. Butter, milk, cheese and pasta fall into this category, as do private label items, which are the retailer’s own brand of more widely known products.

Then there are items which follow a high-low pricing strategy. “Specials” will be discounted for a period of time before returning to their usual price. Confectionery, soft drinks, chips and snacks fall into this category and give shoppers a “price thrill” (Section 4.1.1). The report showed that these variations in price are often not in lockstep between the two major retailers so there is the possibility of getting a product cheaper at this week’s discounter.

However, making such comparisons is difficult and time consuming. While Coles and Woolworths advertise their products and prices online there is no universal identifier in the product description although this is a recommendation of the report as @Belfry noted.

A number of price comparison websites do scrape the retailers. Some, like Zyft, have browser extensions that allow price comparisons for a given product at a given site. Unfortunately, all comparison sites suffer from the lack of that universal identifier and similar products that vary in size but look the same online are hard to tell apart.

Of course all packaged products have an EAN-13 code that is used internally for stock control. Given the code you can probably find the product in Barcode Lookup or Go UPC, which are commercial applications. For a community sourced database there is Open Food Facts. It is said to be less accurate but it seemed to work OK on my minimal testing today.

Some retailers, like Aldi, do not have online shopping or a complete online price list. Aldi is a low cost, low margin, limited stock retailer and it does not believe the expense of an online presence is justified.

“Direct to boot” / “click and collect” are also options for the time poor similar to our discussions on picking up cheap fuel at the right place at the right time. The only cost (for the time being) is two dollars per bag. This can be avoided if you bring your own bags / boxes. Unfortunately using “direct to boot” means you cannot squeeze the avocados.

There are further factors affecting the shopper, like loyalty programs. It is hard to know whether such programs are worthwhile. “Loyalty” makes the shopper less likely to shop around for better or cheaper products elsewhere.

The report goes on to discuss the deals that the big two have with their suppliers. These relate to costs for in house advertising, possible competition from store (private label) brands and the power that comes from having a virtual duopsony in the market for bulk suppliers.

I am not sure where that leaves us in terms of the HLB shopping app.

For the disciplined individual, disciplined by nature or circumstance, the best solution would be to have a monthly meal plan, a weekly shop and do the bulk purchases at Aldi, hope that the secondary shop is over $50 (not too hard these days) and bring your own bags when you click and collect. You might need to scan the EAN-13 when you get home but online shopping might stock the pantry app from the order and Ideally the meal plan should remove the items consumed.

Unfortunately it is hard to be that disciplined and the curious may prefer to just cruise the aisles and damn the expense.

I think you’ve hit all the same areas that I did when I did my initial cursory glance of “what data is available and how would I use it?”.

Barcode formats are also changing soon to a QR code format. It’s likely is that packaging will be dual barcode (“traditional” and the new QR format) for some time, given the massive and global scale of the change. However, this was another reason for me to sit this one out for a while and come back to it in a few years once the dust has settled, the government(s) involved have or have not mandated grocery APIs, and so on.

I toyed with this too, but I figured there’s no point in me setting up grocy or equivalent, particularly if I’m going to invest in a small barcode scanner near the fridge or implement any sort of automation, if the landscape is going to change anyway.

The HLB shopping app goes on the back burner for a while longer :joy:.

Once again @Belfry shows me how far behind the times I am. :snail:

Adding expiry dates, batch numbers and product URLs to the 2D barcodes will make inventory management easier for both retailer and consumer. Also having fresh produce data like volume, weight, best before date in the barcode will improve management of the non-packaged items. I will have to start paying attention to how many products have 2D codes on my future shopping trips.

In terms of local management, I see there are a few apps that scan barcodes and and some integrate with grocy. These would best be used with a dedicated scanner and while one could use a phone or a dedicated handheld device, I think the hands-free option would be best if you had the counter space.

Scanning in and out will continue to be a pain however. The @techman reminds us these days that AI is smart, so I put my last Coles receipt docket under the scanner and got Claude to create a CSV file from the data. I thought it did a great job and managed to handle both packaged and fresh produce. I haven’t progressed to getting Claude to add the EAN-13/GTIN from OpenFoodFacts.org but that’s probably the next step.

Item Quantity Unit Price ($)
CLS Chicken Thigh Cutlet (per kg) per kg 11.19
Carman’s Super Berry 875g 1 each 9.90
Sanitarium Cereal WE 1.2kg 1 each 7.00
Potatoes Creme Gold (0.661 kg @ $5.00/kg) 0.661 kg 3.31
Lemons 2 $1.00 each 2.00
Yumi’s Hommus 500g 1 each 6.50
Chobani 20g Protein 190g 2 $3.00 each 6.00
Continental Cas Asia 60g 2 $3.00 each 6.00
Vine Ripened Tomato 500g 1 each 5.90
Campbell’s Stock Chicken 250ml 2 $1.80 each 3.60
Red Onions (0.411 kg @ $5.50/kg) 0.411 kg 2.26
Cherry Tomatoes 250g 1 each 3.30
Ainsley Harriott Couscous 100g 2 $2.00 each 4.00
Continental Cup Soup 4 Srv 75g 1 each 3.20
Green Zucchini (0.261 kg @ $5.90/kg) 0.261 kg 1.54
Strawberries 250g 1 each 4.00
Garlic (0.041 kg @ $33.00/kg) 0.041 kg 1.35
TOTAL (22 items) 81.05

But I also wondered if I could avoid even this step. I note that Bunnings at checkout allows me to send a link to my receipt to my phone and I can then add the purchases to my online Bunnings’ inventory or keep it as a download. Will C/WW/Aldi ever enable that?

So the options for input look to be online ordering, scanning at checkout (with emailing the data), rescanning the items at home or processing the paper receipts. It is not clear which is best now or will be in the future.

It’s even easier now I’m happy to report.

Take a pic of the receipt with your phone and give it to Qwen 3.8 27B (in a Quant that runs on your PC) as a LOCAL only agent (free tokens).

Qwen now has image recognition, vastly improved reasoning and this latest release really KICKS!

I have replaced two Agents with it in my 'Fossilcrew" and it scans pics in a second with detail and accuracy I’ve not seen before.

While you’re at it ask it to add it to the database and compare the receipt to your Sqlite database of previous receipts and give price changes based on all the sly tricks, are the weight smaller and the price the same etc ?

If you don’t have a Sqlite database for that purpose yet, just ask Qwen 3.8 27B to make one first, then do the above :slight_smile:

Cheers,
Terry

OK. I’m saving my pennies for a RTX 3090 with 24GB RAM. I am told that’s sufficient to run Qwen 3.8 27B.

They say that apps are passé. These days you just get AI to do everything for you.

I’m running it nicely on two RTX3060’s (24GB VRAM total) on a RYZEN 5500 and 74GB ram

We’ve been here before, once we had to hand crank our cars to start them.

Who seriously misses doing that nowadays ?

Since we’ve been discussing duopolies in grocery shopping, petrol pricing, apps and AI, HLBers may be interested in Alan Kohler’s interview with Gina Cass-Gottlieb of the ACCC.

While the big two only have a NPAT of 3%, the ACCC inquiry found that their margins were high by international standards. Cass-Gottlieb is keen for consumers to have sufficient information to make an informed choice on the products they buy. She adheres to the view that sunlight is an excellent disinfectant.

The ACCC Supermarket Inquiry was received by the government last year and in early 2026 Treasury subsequently sought feedback from consumers and providers on various outstanding proposals.

Choice supported a single standard API for online shopping for the big players - Coles, Woolworth’s and now probably Aldi.

The Business Council of Australia was less keen on this idea. It argued that the focus should be on reducing compliance and regulatory costs and “Requiring bespoke API systems would impose substantial cost and technical complexity, particularly given supermarkets carry tens of thousands of products with frequently changing prices and promotions.” They felt that the information currently available on supermarket websites and web-scraping apps was sufficient.

The Law Council of Australia sided with the BCA and recommended avoiding “mandating APIs, which would impose a significant compliance burden on supermarkets”.

Two things struck me with all this. There was no mention of EANs or GTINs. Perhaps this is just considered an implementation issue. Yet GS1 is pretty keen on the idea of GTIN’s being widely available by 2028 and would presumably love some governmental assistance.

I was also surprised with the view that implementing an API for products was such a big issue for the likes of Coles and Woolies. I would have thought that all their inventory would be readily tracked from arrival in the loading bay to heading out past the scanner and that there would be some interface for managing all that. Do the technical members of HLB also think this odd?

I was several paragraphs above at “The Business Council of Australia…” and was already calling rubbish on that. I 100% agree with you, @zeeclor. The systems behind our big supermarkets would have all the data there, and any mandated APIs would be just linking to that and making available… data already publicly available via other means.

I don’t have time to do a deeper dive right now, but the claims of “burden” angried up the blood a bit :rofl:. The banks and energy companies undoubtedly moaned about CDR, and I’m sure the fuel companies grumbled about the various state based fuel API schemes too. The sky didn’t fall. Woolworths operates what would have to be one of the most sophisticated data and analytics platforms in the southern hemisphere. No doubt Coles is similar. I’m sure they can both suck up adding an API endpoint.

I reckon that your use of that fuel API was the secret sauce in your fuel solution @Belfry , and in my opinion it’s precisely where our grocery ambitions are being thwarted.

I’m disappointed to hear that this “Law Council of Australia” sided with the “Business Council of Australia”, and apparently that’s been enough to block the move. I’m not aware of too many “substantial costs” that the supermarket chains haven’t automatically and entirely passed along to the consumer. We’re paying more for smaller or emptier packaging, then paying for the bags and loading them ourselves. If the supermarket chains could manipulate us into it, they have us vacuum, mop and clean the toilets on the way out.

I agree that it would be an expensive exercise for the supermarkets, but that’s just the first “move on the chess board”. The next move is that the consumer would absorb that cost. The upside of that is that is that at long last we would be able to make informed choices instead of being overwhelmed by wave after wave of advertising driven manipulation. Informed consumers would stimulate competition between the supermarkets, which in turn would drive prices down. That competition part is the move on the chess board that the is being anticipated and artfully avoided by the supermarket chains.

Unfortunately, i feel that the consumer continues to be poorly represented. The ACCC and Choice are clearly inadequate representatives so far. Wasn’t democracy supposed to play a role here somewhere?

Cynical rant concluded…

Following in @Belfry’s footsteps of using the HLB Discourse as an archive, this is the Law Council of Australia’s response to the Supermarket inquiry. They did not support mandatory APIs as mentioned above.

I also note that there was a submission to the inquiry by UpUp a tech startup from 2024 that aimed to let shoppers compare prices for Coles and Woolworths. I tried the app today but did not get anywhere with it. In any event I don’t think a mobile phone app is the right interface for shopping comparisons.

Agree. I think the real value will be in an open API of some sort for tinkerers such as ourselves to start interfacing with.

The other wild card here is: What will have to participate in any hypothetical API? All fuel retailers have to participate in the fuel API, and all banks and energy companies do CDR. The barrier to entry for these outlets is relatively high, and the definitions are relatively clear cut.

However, What’s a grocery store? What’s a supermarket? Woolworths, Coles, Aldi, are pretty clear cut. I believe (but am not 100% sure) that IGA and SPAR are independent franchisees, with a mix of some company stores, and can set prices independently. Are they “supermarkets” or are they “independent small businesses” (or whatever definition)? Fresh & Save (awesome, btw, if you’re reading this and haven’t checked them out yet), probably a “supermarket”. What about Costco? Friendly Grocer? Foodworks? The local corner store? The local fruit shop + butcher co-located in the same store with a stand from the local bakery just inside the door? I think that’s where things start to get messy. The Law Council’s submission (linked by @zeeclor) makes reference to “large supermarkets”, which is not an unreasonable place to start. However, what are the unintended consequences of having apps only having access to a certain segment of the market via API? Does the independent grocer now miss out because all the fancy new shopping apps only source deals from the large end of town? etc, etc, etc… my very long winded point is that I don’t think this is as cut and dry as the other open data problems recently tackled. As a “tinkerer” and potential customer of said API, I’d love it to happen though!

One thing did jump out at me from the Law Council’s submission:

As the Australian Competition and Consumer Commission (ACCC) has observed,
supermarkets such as Coles and Woolworths already permit third‑party operators to
scrape their online prices.

After reading that, I’d like to do a deeper dig on that again at some stage. That didn’t seem to be the case in my quick first pass when I had a look at it earlier in the year. I wonder; Does “permit” in the Law Council’s submission mean “allow and publish openly” or does it mean “not actively playing cat-and-mouse despite this being against the T&Cs of the website and something that should be discouraged”? I got the impression from the GitHub projects I’d looked at to try and tackle some of these APIs, that this was all somewhat fragile and reverse engineered from supermarkets’ own apps and websites.

Not being very smart but being very lazy I asked Claude.

There’s no legal exemption or special deal for most of these projects. A few patterns:

They hit undocumented internal APIs, not HTML pages. Both Coles and Woolworths render their sites via JSON APIs that the frontend calls (product listings, pricing, search).

Some sidestep it entirely by using third-party data providers (Bright Data being a common one) who’ve apparently negotiated or built enough infrastructure to sell “Woolworths price tracking” as a service, and others cross-reference against sites like buywisely.com.au that already expose an API rather than hitting the retailer directly.

Your Claude link mentioned this, and https://pricesapi.io/ seems to be sitting underneath.

Will sign up and have a play with the personal (free) tier at some stage. Australia supported, and both Coles and Woolworths are listed on their site. I assume they’re doing the scraping or reverse engineering to build up their own price database. Thank you @zeeclor for the nudge in the right direction.

I’m stuck at home with some sort of horrid flu thing and curiosity got the better of me.

I signed up, got a Personal Plan API key, and tried a few calls along the lines of e.g., “2L milk”:

curl -H "Authorization: Bearer xxx" "https://api.pricesapi.io/api/v1/products/search?q=2L%20milk"

The docs are great and the API is simple :+1:.

Responses include product data, seller, price, stock status, a base64 encoded image, etc.

e.g.,

"products":[{"position":1,"pid":11689709,"gpcid":"5667449833069158248","gid":"5571627633148997718","title":"Coles Full Cream Milk 2L","image":"data:image/webp;base64

*snip*

"price":3.55,"currency":"AUD","condition":null,"source":"Coles Supermarkets","multi_store":false,"rating":null,"reviews":null,"delivery":null,"tags":[],"nearby_distance_km":null,

Threw together some very quick and dirty YAML to pull the REST, parse the JSON, and slap the resulting sensors into a new card on a new HA dashboard:

Doesn’t seem to be a way to filter to proximity (it doesn’t get any more granular than “AU”), and I also see lots of results for 600mL milk, 1L milk, 3L milk, etc. with no way to filter that out without implementing something client side or using an intermediary (e.g., a Cloudflare Worker). Also seems to run into the issue where several places are pricing independently, as discussed a few posts ago (e.g., Foodworks franchisees in this screenshot all having their own prices).

Searching a few random UPC barcodes for food items in the fridge got me car parts, computer parts, tyres, shoes, rugs… no food items. Searches via UPC doesn’t look like it works. I didn’t try searching via URL (as documented in the PricesAPI docs) as I tried to keep a grocery focus.

Probably fantastic for really specific SKUs (e.g., iPhone 17 Pro Max 512GB in a specific colour), but maybe not as useful for grocery staples? No idea. Will throw this one back to the HLB brains trust after @zeeclor’s promising lead earlier today. Hopefully a clearer head on the Discourse can pick up the baton and do a better job than what I’ve managed! At the very least I think this is going to need some sort of intermediary layer to “grocerise” the dataset into something more useful for Home Assistant type automations.

That’s disappointing but probably not surprising.

On a brighter note however, Claude and I (mostly Claude) have been working on the data input part for the Shopping App. It turns out AI enhanced scanning of the shopping docket works pretty well. Claude is writing the app in python and is integrating it with Grocy and Mealie.

I have appended the current version of the README below but it is constantly changing as Claude adds more functionality. A few things to highlight.

Depletion

Claude notes that most pantry apps fail because consumed items are not removed. The app addresses this by deleting items from a consumed meal / recipe or scanning empty packets at the end of the meal (another task for the dishwasher) or manual removal of the expired items that Grocy flags.

Fresh produce

The shopping docket approach looks like a great solution for fresh produce that does not have an EAN-13. It will be interesting to see how well it works in practice.

Grocy database population

The setup so far has been made on only four shopping dockets. The difference between the model and what’s in the pantry should shrink over time as more shopping dockets are added and as more recipes are added to Mealie. Again time will tell.

Next Stage

For the time being I plan to just let the database populate with more dockets before even considering doing an actual stocktake of the pantry. I will also add dockets from Aldi, IGA and others grocery stores.

Claude also recommends using Barcode Buddy to integrate with Grocy for managing item consumption. This is said to be a better workflow than relying on Grocy’s inbuilt scanning option.

I’ll keep the thread posted.

README.md


theShoppingApp

Turn a scanned supermarket docket into tracked pantry stock, and take it back out again as you cook.

Australian supermarket receipts (Coles and Woolworths) are read by a vision model into structured data, checked against their own printed totals, matched to known products, and posted into Grocy as stock purchases with real prices. Recipes live in Mealie; cooking one consumes its ingredients from Grocy.

stock in    scan.pdf ──▶ extract ──▶ validate ──▶ resolve ──▶ post
                         (vision)   (arithmetic)  (aliases)   (Grocy API)

stock out   Mealie recipe ──▶ parse ──▶ resolve ──▶ consume
                              (NLP)     (foods)     (Grocy API)

Stock also leaves by two other routes: consume.py --overdue for anything past its date, and a barcode scan of the empty packet.

Why it works

Receipts self-validate. Every receipt carries redundant arithmetic — line items that must sum to the printed total, per-unit prices that must reconstruct each line, taxable lines that must produce the printed GST, and a printed item count. Four independent checks, all from numbers on the receipt itself, with no external ground truth needed.

That means a clean extraction can be auto-accepted and only doubtful ones queued for review. A single misread digit anywhere breaks at least one check.

Entity resolution is the actual problem. A docket has no barcodes — just truncated store descriptions:

WW CRUSHED TOM 400G        1.10
CLS CHKN THIGH CUTLE PERKG 11.19

Coles hard-truncates descriptions to 20 characters, so the full product name is not recoverable from the receipt at all. The fix is a learned lookup table: docket text → product, confirmed once per new product and reused forever after. New products land in a review queue; everything already known resolves silently.

Depletion is where stock records usually die. Buying is easy to record and using is not, so counts drift into fiction within a month. Three capture points share the load: cooking a recipe, scanning an empty packet, and clearing anything that has passed its date. Each writes an undo handle, because the failure mode of a depletion tool is silently removing something you still have.

Anything used in parts is stocked by weight. Grocy reads a recipe amount in the product’s stock unit, so a 200 g block of parmesan stocked as one Piece forces every partial use to be written as 0.15 Piece. Stocking it in grams makes recipes natural, and a pack size converts the docket line at purchase time.

What it does

  • Extracts receipts from PDF or image scans, several receipts per page if needed
  • Validates each one against its own printed control totals before anything is written
  • Resolves docket lines to Grocy products through a learned alias table
  • Posts purchases with correct prices, quantities and computed best-before dates
  • Consumes stock that has passed its due date, individually targeted by stock entry
  • Cooks a Mealie recipe or a day’s meal plan, deducting what it can from stock
  • Scans barcodes, recorded in the catalogue so one scan means one pack
  • Records every run as a timestamped undo handle, so any batch can be reversed

Handles weighed items (0.381 kg NET @ $4.20/kg), multi-buys (2 @ $1.80 EACH), GST flags, promotional pricing, and per-store price history across chains.

Setup

Requires Python 3.11+, a running Grocy instance, and an Anthropic API key. Cooking recipes additionally needs a Mealie instance and an API token.

python3 -m venv .venv
.venv/bin/pip install anthropic pydantic

Put credentials in ~/.config/theshoppingapp/env (mode 600, never in the repo):

export GROCY_URL=https://grocy.example.com
export GROCY_API_KEY=...      # Grocy → Settings → Manage API keys
export ANTHROPIC_API_KEY=...
export MEALIE_URL=https://mealie.example.com
export MEALIE_TOKEN=...       # Mealie → profile → API Tokens

Grocy needs a real hostname over HTTPS if you want to scan barcodes: the browser only grants camera access in a secure context, so the camera button is dead over plain HTTP.

Seed Grocy with quantity units, locations and your product catalogue:

source ~/.config/theshoppingapp/env
python3 grocy/seed_units_locations.py
python3 grocy/seed_products.py

Both scripts are idempotent — re-running only fills gaps.

Use

source ~/.config/theshoppingapp/env

# see what a docket would post, without writing anything
.venv/bin/python extractor/pipeline.py scans/Coles-1.pdf --dry-run

# extract and post
.venv/bin/python extractor/pipeline.py scans/Coles-1.pdf

# clear stock that has passed its due date
.venv/bin/python extractor/consume.py --overdue --dry-run
.venv/bin/python extractor/consume.py --overdue

# what lapses in the next week?
.venv/bin/python extractor/consume.py --overdue --as-of 2026-09-01 --dry-run

# run history, and undo
.venv/bin/python extractor/pipeline.py --runs
.venv/bin/python extractor/pipeline.py --undo

# cook a recipe: deduct what it uses from stock
.venv/bin/python extractor/cook.py --recipe baked-ziti --dry-run
.venv/bin/python extractor/cook.py --recipe baked-ziti
.venv/bin/python extractor/cook.py --mealplan            # everything planned today
.venv/bin/python extractor/cook.py --undo

# record a barcode against a product, then push it to Grocy
python3 grocy/add_barcode.py "Sirena Tuna"               # prompts, then scan
python3 grocy/add_barcode.py --list                      # coverage, and what is missing
python3 grocy/add_barcode.py --import                    # pull in ones added via Grocy

Full flag reference: extractor/README.md.

Adding a new product

The first time you buy something, its docket line won’t resolve:

resolved 1/3 lines ($3.70 of $11.40)
  REVIEW  line 1: no product mapped for 'Gravox Gravy Schnitzel 165g'

Add an entry to grocy/catalogue.json with the raw docket text as its alias, re-run seed_products.py, and it resolves from then on. That is the one-time cost per product.

Recipe ingredients work the same way. A dry run lists what it could not place:

REVIEW  not stocked: fish sauce -- 1 tbsp fish sauce

Add the Mealie food name to that product’s recipe_foods and it resolves next time. A food may name several products — two sizes of cream, two brands of stock — and the one with the stock is chosen when you cook.

Safety

Grocery data is easy to corrupt quietly, so the tooling refuses rather than guesses:

  • Validation gates posting. A receipt failing its arithmetic is reported and skipped.
  • Duplicate receipts are refused, keyed on chain, store, timestamp and total — rescanning the same docket cannot double-count it.
  • Merge-aware undo. Grocy merges a purchase into a matching existing stock entry; undoing that would delete the pre-existing stock too, so undo refuses when it would.
  • Unknown unit pairings are refused, never guessed — they go to review instead.
  • Every stock entry records its source docket line, so any row traces back to its receipt.
  • Cooking twice in a day is refused without --force, and a recipe short of stock consumes nothing unless --allow-short.
  • Volume is never converted to mass without a recorded density — “1 cup parmesan” goes to review rather than inventing a number.
  • Runs are sealed when a product’s stock unit changes, because replaying them would restore a quantity in units no longer used.

Reliability

Verified against four receipts / 48 line items, reproduced exactly across repeated runs.

Recipe coverage is honest rather than flattering: about 18% of ingredients across the recipe collection resolve to a Grocy product. The rest are pantry staples — salt, olive oil, flour, soy sauce — that have never appeared on a docket and so are not Grocy products at all. What does resolve is the perishable end: meat, dairy, produce, which is where a stock count is worth having. Coverage climbs as more dockets are processed.

Vision extraction is not deterministic. In one extraction of a two-receipt page the model returned an empty stub for the second receipt — a failure that passed every arithmetic check, because zero lines sum to a declared zero. Structural checks now reject that. Scanning one docket per page avoids the failure mode entirely.

Layout

extractor/      the pipeline: extract, validate, resolve, post, consume, cook, compare
grocy/          product catalogue, alias/food/barcode tables, idempotent seed scripts
                plus add_barcode.py and migrate_stock_units.py
phase0/         regression fixtures — 4 receipts, arithmetically verified
scans/          docket scans
runs/           timestamped undo handles (not tracked)