/meal-flyer — Store Flyer Parser
Parse a weekly grocery store flyer and extract sale items mapped to canonical ingredient IDs.
Announce at start: "Processing store flyer..."
Step 1: Get Flyer Input
The user provides flyer data in one of two ways:
Option A: PDF File
If the user provides a file path ending in .pdf:
- Read the PDF using the Read tool
- For PDFs over 20 pages, paginate: read pages 1-20, then 21-40, etc.
- Focus on food items — skip pages that are clearly non-food (cleaning supplies, pharmacy, seasonal items)
- After the flyer markdown is successfully written and verified, delete the source PDF so raw flyer files do not accumulate in the repo
Option B: Pasted Text
If the user pastes text from a store website or flyer:
- Use it directly
If no input is provided, ask: "Please provide the store flyer — either paste the text or give me the path to a PDF file."
Step 2: Load Config and Identify the Store
Read data/config.yaml to get the user's store list.
Determine which store this flyer is from:
- Look for store name in the flyer content
- If not obvious, ask: "Which store is this flyer from?" and list the stores from config as options
Step 3: Determine Date Range
Look for a validity date range in the flyer content (e.g., "Valid 3/22 - 3/28", "Weekly Specials March 22-28").
- If found, use the start date for the filename
- If not found, use today's date
- Record the full date range for frontmatter
Step 4: Load Canonical Ingredients
Read data/canonical-ingredients.yaml from the project root. This is the normalization reference for mapping sale items.
If the file does not exist: Stop and tell the user: "data/canonical-ingredients.yaml is missing. Run /meal-setup first to initialize the project, then re-run /meal-flyer."
Step 5: Extract Sale Items
For each sale item in the flyer, extract:
| Field | What to capture | Examples |
|---|---|---|
| Item description | As printed in the flyer | "Boneless Skinless Chicken Thighs Family Pack" |
| Price | The sale price | $2.49, $8.99 |
| Unit | Price unit | /lb, /each, /pint, /dozen, /bag |
| Deal type | Type of promotion | Regular Sale, BOGO, Manager's Special, 2 for $5 |
Price normalization rules:
- "2 for $5" → price: $2.50, unit: /each, deal type: "2 for $5"
- "Buy One Get One Free" / "BOGO" → price: [listed price], deal type: "BOGO (effective 50% off)"
- "Save $2" → note the savings but use the final sale price if shown
- "$X.XX/lb" → price: $X.XX, unit: /lb
- If price per unit can't be determined, record as-is with the deal description
Filtering rules:
- Include: All food and beverage items
- Skip entirely: Non-food items (cleaning supplies, paper goods, pet food, pharmacy, health/beauty). Do not include them in the Sale Items table. Track the count for the confirmation summary only.
Step 6: Map to Canonical Ingredients
For each food item:
Match against canonical-ingredients.yaml — find the closest canonical ID:
- "Boneless Skinless Chicken Thighs Family Pack" →
chicken_thigh - "Fresh Atlantic Salmon Fillets" →
salmon - "Grape Tomatoes Pint" →
cherry_tomato - "Greek Yogurt Assorted" →
greek_yogurt
- "Boneless Skinless Chicken Thighs Family Pack" →
Unmappable food items — if a food item doesn't match any canonical ingredient:
- Mark with
?in the canonical ID column - These go into an "Items Needing Review" section for the user to decide if they should be added to the canonical list
- Mark with
Non-food items — already excluded in Step 5; do not include any row for them in the table
Step 7: Generate Summary
Identify which ingredients from the core set (~24 items in canonical-ingredients.yaml) are on sale this week. List them with price and store name.
If no core ingredients are on sale, note: "No core ingredients on sale this week."
Step 8: Detect Likely Loss Leaders
Identify items priced aggressively low — likely below store cost to drive foot traffic.
Method 1 — Savings Heuristic (always available)
For each sale item with a "Save $X" value: compute savings_pct = save_amount / (sale_price + save_amount). Flag as loss leader if savings_pct >= 0.35.
Method 2 — Price History Comparison (when data exists)
Read data/receipts/price-history.yaml if it exists.
For each sale item that has a canonical ID (not ?):
- Look up the canonical ID in the price history
- Compute the average price from all ledger entries for that item
- If the sale price is >30% below the average, flag as a likely loss leader
If the price history file doesn't exist, skip Method 2 silently.
Output
If any items are flagged, add a new section to the flyer markdown between "Core Ingredients On Sale This Week" and "Items Needing Review":
## Likely Loss Leaders
| Item | Sale Price | Regular/Avg Price | Savings | Method |
|------|-----------|-------------------|---------|--------|
| [Item] | $X.XX/unit | $Y.YY (regular) | NN% | heuristic |
| [Item] | $X.XX/unit | $Y.YY (avg from N receipts) | NN% | price-history |
| [Item] | $X.XX/unit | $Y.YY (regular) / $Z.ZZ (avg) | NN% | both |
Consider building meals around these items — they're priced to get you in the store.
If an item is flagged by both methods, show it once with Method = "both" and show both the regular and average prices.
If no items meet either threshold, omit this section entirely.
Step 9: Check for Collisions
Generate filename: data/flyers/YYYY-MM-DD-<store-slug>.md
- Store slug:
market-basketorhannafords - Example:
data/flyers/2026-03-22-market-basket.md
If the file already exists, ask: "A flyer for [store] on [date] already exists. Overwrite, save as [date]-[store]-2.md, or cancel?"
Step 10: Write the Flyer File
Ensure the directory exists: run mkdir -p data/flyers via Bash before writing.
Write to data/flyers/YYYY-MM-DD-<store-slug>.md using this format:
---
store: [Store Name]
date_range: [YYYY-MM-DD to YYYY-MM-DD]
source: [weekly-flyer-pdf or pasted-text]
---
## Sale Items
| Item | Canonical ID | Price | Unit | Deal Type |
|------|-------------|-------|------|-----------|
| [Item description] | [canonical_id or ?] | [$X.XX] | [/unit] | [deal type] |
## Core Ingredients On Sale This Week
- [canonical_id] — $[price]/[unit] ([Store Name])
- [canonical_id] — $[price]/[unit] ([Store Name])
<!-- Include this section only if Step 8 flagged any loss leaders; omit entirely if none -->
## Likely Loss Leaders
| Item | Sale Price | Regular/Avg Price | Savings | Method |
|------|-----------|-------------------|---------|--------|
| [Item] | $X.XX/unit | $Y.YY (regular) | NN% | heuristic |
Consider building meals around these items — they're priced to get you in the store.
## Items Needing Review
- [Item description] → marked `?` ([reason or suggestion for canonical mapping])
If the source input was a local PDF file and the markdown flyer was written successfully, delete the PDF after saving the markdown output. Do not keep a separate archived PDF copy.
Step 11: Confirm
Output a summary:
Flyer saved: data/flyers/YYYY-MM-DD-<store-slug>.md
Store: [Store Name]
Date range: [date range]
Items extracted: [N total]
Mapped to canonical: [M items]
Marked for review (?): [K items]
Skipped (non-food): [J items]
Core ingredients on sale:
- [list or "none"]
Loss leaders detected: [N items, or "none"]
- [list if any]
Source PDF deleted: [yes/no/not-applicable]
Review items marked with ? and decide if they should be added to data/canonical-ingredients.yaml.