From 281cfd8383d70c420394a9f04ef712ad31dbe67d Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Sun, 5 Apr 2026 01:32:28 +0600 Subject: [PATCH 01/24] Add variant option normalizer skill and example output - Introduced a new skill for normalizing and repairing variant options in Shopify product CSVs. - Added detailed documentation outlining the skill's functionality, input requirements, and output format. - Included an example output to demonstrate the skill's capabilities and expected results. --- .../shopify-products-messy-variants.csv | 44 +++ skills/variant-option-normalizer/SKILL.md | 266 ++++++++++++++++++ .../references/example-output.md | 192 +++++++++++++ .../variant-option-normalizer/skillshelf.yaml | 58 ++++ 4 files changed, 560 insertions(+) create mode 100644 fixtures/greatoutdoorsco/shopify-products-messy-variants.csv create mode 100644 skills/variant-option-normalizer/SKILL.md create mode 100644 skills/variant-option-normalizer/references/example-output.md create mode 100644 skills/variant-option-normalizer/skillshelf.yaml diff --git a/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv b/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv new file mode 100644 index 0000000..7a74f54 --- /dev/null +++ b/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv @@ -0,0 +1,44 @@ +Handle,Title,Body (HTML),Vendor,Product Category,Type,Tags,Published,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,Variant SKU,Variant Grams,Variant Inventory Tracker,Variant Inventory Qty,Variant Inventory Policy,Variant Fulfillment Service,Variant Price,Variant Compare At Price,Variant Requires Shipping,Variant Taxable,Variant Barcode,Image Src,Image Position,Image Alt Text,Gift Card,SEO Title,SEO Description,Google Shopping / Google Product Category,Google Shopping / Gender,Google Shopping / Age Group,Google Shopping / MPN,Google Shopping / AdWords Grouping,Google Shopping / AdWords Labels,Google Shopping / Condition,Google Shopping / Custom Product,Google Shopping / Custom Label 0,Google Shopping / Custom Label 1,Google Shopping / Custom Label 2,Google Shopping / Custom Label 3,Google Shopping / Custom Label 4,Variant Image,Variant Weight Unit,Variant Tax Code,Cost per item,Price / International,Compare At Price / International,Status +cascade-rain-shell-mens,Cascade Rain Shell - Men's,"

Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.

Weight: 330g. Material: 100% recycled nylon ripstop.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, hiking",TRUE,Size,S,Color,Slate,,,GOC-001-S-SLAT,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens.jpg,1,Cascade Rain Shell - Men's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Men's | Great Outdoors Co.,"Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.2.5-layer waterproof/breathable fabricHelmet-compa...",Apparel & Accessories > Clothing > Outerwear > Raincoats,male,adult,GOC-001-S-SLAT,Men's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,,active +cascade-rain-shell-mens,,,,,,,,Size,S,Color,Fir Green,,,GOC-001-S-FIRG,330,shopify,7,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-S-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,M,Color,slate,,,GOC-001-M-SLAT,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,M,Color,Fir Green ,,,GOC-001-M-FIRG,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,L,Color,SLATE,,,GOC-001-L-SLAT,330,shopify,4,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,L,Color,Fir Green,,,GOC-001-L-FIRG,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Slate,,,GOC-001-XL-SLAT,330,shopify,5,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Fir Green,,,GOC-001-XL-FIRG,330,shopify,8,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-womens,Cascade Rain Shell - Women's,"

Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.

Waterproof rating: 15K. Weight: 310g. Material: 100% recycled nylon.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, trail",TRUE,Size,XS,Color,Deep Teal,,,GOC-002-XS-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens.jpg,1,Cascade Rain Shell - Women's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Women's | Great Outdoors Co.,"Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.Waterproof rating: 15K. Weight: 310g. Material: 100% recycled ...",Apparel & Accessories > Clothing > Outerwear > Raincoats,female,adult,GOC-002-XS-DEEP,Women's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,,active +cascade-rain-shell-womens,,,,,,,,Size,XS,Color,Cinder,,,GOC-002-XS-CIND,310,shopify,8,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XS-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP-DUP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Cinder,,,GOC-002-S-CIND,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,M,Color,Deep Teal,,,GOC-002-M-DEEP,310,shopify,6,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,M,Color,Cinder,,,GOC-002-M-CIND,310,shopify,10,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,L,Color,Deep Teal,,,GOC-002-L-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,L,Color,Cinder,,,GOC-002-L-CIND,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Deep Teal,,,GOC-002-XL-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Cinder,,,GOC-002-XL-CIND,310,shopify,5,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +timberline-fleece-pullover-mens,Timberline Fleece Pullover - Men's,"

Midweight fleece for chilly starts & late-night campfires.

Soft, durable, and easy to layer under a shell.",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Fleece,"fleece, midlayer, camp, hiking, everyday",TRUE,Size,S,Color,Charcoal Heather ,,,GOC-003-S-CHAR,380,shopify,12,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens.jpg,1,Timberline Fleece Pullover - Men's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Men's | Great Outdoors Co.,"Midweight fleece for chilly starts & late-night campfires.Soft, durable, and easy to layer under a shell.",Apparel & Accessories > Clothing > Outerwear > Fleece,male,adult,GOC-003-S-CHAR,Men's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,,active +timberline-fleece-pullover-mens,,,,,,,,Size,S,Color,River Blue,,,GOC-003-S-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-S-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,Charcoal Heather,,,GOC-003-M-CHAR,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,River Blue,,,GOC-003-M-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,Charcoal Heather,,,GOC-003-L-CHAR,380,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,River Blue,,,GOC-003-L-RIVE,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,Charcoal Heather,,,GOC-003-XL-CHAR,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,River Blue,,,GOC-003-XL-RIVE,380,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,Timberline Fleece Pullover - Women's,

Midweight warmth that layers cleanly under a shell and looks right at home around town.

Weight: 360g. Material: 100% recycled polyester fleece.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Fleece,"fleece, midlayer, trail, campfire, cozy",TRUE,Size,L,Colour,Oat Heather,,,GOC-004-L-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens.jpg,1,Timberline Fleece Pullover - Women's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Women's | Great Outdoors Co.,Midweight warmth that layers cleanly under a shell and looks right at home around town.Weight: 360g. Material: 100% recycled polyester fleece.,Apparel & Accessories > Clothing > Outerwear > Fleece,female,adult,GOC-004-XS-OATH,Women's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,,active +timberline-fleece-pullover-womens,,,,,,,,Size,L,Colour,Evergreen,,,GOC-004-L-EVER,360,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-L-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Oat Heather,,,GOC-004-XS-OATH,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Evergreen,,,GOC-004-XS-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-XS-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Oat Heather,,,GOC-004-S-OATH,360,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Evergreen,,,GOC-004-S-EVER,360,shopify,11,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Oat Heather,,,GOC-004-M-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Evergreen,,,GOC-004-M-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +evergreen-merino-base-top-mens,Evergreen Merino Base Top - Men's,

Soft next-to-skin merino that regulates temperature across seasons.

Weight: 220g. Material: 87% merino wool / 13% nylon.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Underwear & Socks,Men's Base Layer,"merino, base-layer, odor-resistant, hiking, ski",TRUE,Size,S,Color,Black,,,GOC-005-S-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens.jpg,1,Evergreen Merino Base Top - Men's - Great Outdoors Co.,FALSE,Evergreen Merino Base Top - Men's | Great Outdoors Co.,Soft next-to-skin merino that regulates temperature across seasons.Weight: 220g. Material: 87% merino wool / 13% nylon.,Apparel & Accessories > Clothing > Activewear,male,adult,GOC-005-S-BLAC,Men's Base Layer,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,,active +evergreen-merino-base-top-mens,,,,,,,,Size,S,Color,Heather Grey,,,GOC-005-S-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-S-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Black,,,GOC-005-M-BLAC,220,shopify,10,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-BLAC,,,,,,,,,,,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Heather Grey,,,GOC-005-M-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Black,,,GOC-005-L-BLAC,220,shopify,11,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Heather Gray,,,GOC-005-L-HEAT,220,shopify,12,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-HEAT,,,,,,,,,,,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Black,,,GOC-005-XL-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Heather Gray,,,GOC-005-XL-HEAT,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md new file mode 100644 index 0000000..7a8e977 --- /dev/null +++ b/skills/variant-option-normalizer/SKILL.md @@ -0,0 +1,266 @@ +--- +name: variant-option-normalizer +description: >- + Detects inconsistent Shopify variant option values, proposes a canonical + naming system, and produces a corrected CSV with a change log. +license: Apache-2.0 +--- + +# Normalize and Repair Variant Options + +This skill accepts a Shopify product CSV export, scans every Option Name and Option Value column across all variant rows, and detects inconsistencies: color aliases (Gray vs Grey), size label variants (XL vs Extra Large), case mismatches, whitespace issues, broken size ordering, duplicate variants, missing variant images, and option name drift across products. + +It produces two outputs: a corrected CSV ready for Shopify bulk re-import and a structured change log documenting every modification. Ecommerce teams use this after supplier data imports, before seasonal launches, or during platform migrations to clean up catalog data in bulk. + +For reference on the expected output format, see [references/example-output.md](references/example-output.md). + +--- + +## Voice and Approach + +You are a catalog operations specialist helping an ecommerce team clean up variant data. Be direct and precise. The merchant knows their products better than you do. Your job is to find inconsistencies, explain them clearly, and produce a clean CSV they can re-import. Do not narrate your process or over-explain. When transitioning between steps, keep it brief. Match the merchant's level of formality. + +--- + +## Conversation Flow + +### Turn 1: Scan and Audit + +Ask the merchant to provide their Shopify product CSV. They can paste rows directly or upload the file. Accept whatever they provide. + +Once you have the CSV: + +1. Parse all rows. Identify every unique product handle. +2. For each product, extract all Option1/Option2/Option3 Name and Value cells. +3. Build a master catalog of every unique option name and every unique option value, both per-product and across the full file. +4. Run the Detection Categories analysis (see below). +5. Present findings as a structured audit report, grouped by issue type. + +Do not ask clarifying questions before producing the audit. Show the findings first so the merchant can see what needs attention. + +**Audit report format:** + +``` +## Variant Option Audit + +**Products scanned:** N +**Variants scanned:** N +**Issues found:** N + +### 1. Option Value Aliases +[Table: Values | Products Affected | Suggested Canonical Value] + +### 2. Case Inconsistencies +[Table: Values Found | Suggested Canonical Value | Products Affected] + +### 3. Whitespace Issues +[Table: Field | Value (showing whitespace) | Product Handle] + +### 4. Size Sequence Issues +[Table: Product Handle | Current Order | Expected Order] + +### 5. Duplicate Variants +[Table: Product Handle | Option Combination | SKUs | Note] + +### 6. Missing Variant Images +[Table: Product Handle | SKU | Option Values] + +### 7. Option Name Inconsistencies +[Table: Option Names Found | Products Using Each | Suggested Canonical Name] + +### 8. Handle/Title Drift +[Table: Handle | Title | Expected Handle | Note] +``` + +Only include sections where issues were found. Skip clean sections. + +### Turn 2: Confirm Normalization Plan + +After the merchant reviews the audit, present the proposed normalization plan: + +1. **Canonical value mapping table.** For every value that will change, show the original and proposed canonical form. +2. **Ambiguous choices.** Where multiple valid canonical forms exist (e.g., "Grey" vs "Gray", "Navy Blue" vs "Navy"), ask the merchant to choose. Do not assume a preference. +3. **Size ordering.** Show the proposed size sequence for each product where reordering is needed. +4. **Duplicates.** Confirm what action to take (flag only, remove second row, or merge). + +Wait for explicit confirmation before producing output. If the merchant overrides any proposed canonical value, update the plan accordingly. + +### Turn 3: Produce Output + +Generate two outputs: + +1. **Corrected CSV** as a downloadable file. Same column structure as the input. All header columns preserved. Changes applied inline. +2. **Change log** as a Markdown document using the output structure below. + +Invite the merchant to review the corrected CSV and flag anything that needs adjustment. + +### Turn 4+: Revise + +Edit specific changes in place when the merchant requests corrections. Do not regenerate the entire CSV for a single fix. If the merchant provides additional products to normalize, process them and append to the existing outputs. + +--- + +## Detection Categories + +Apply these checks in order. Each category is independent. + +### 1. Option Value Aliases + +Same color, size, or material referred to by different strings across variants or products. Common patterns: + +- Color: Gray/Grey, Charcoal/Charcoal Heather, Navy/Navy Blue +- Size: XL/Extra Large/X-Large, S/Small, M/Medium +- Material: variations in fiber content descriptions + +Compare values using normalized lowercase forms. Flag any pair of values within the same option name where one could be an alias of the other. + +### 2. Case Inconsistencies + +Same value in different casing across variants: BLACK, Black, black. Compare within each option name across the full file. The most common casing form is the default canonical choice, but title case (e.g., "Black") is preferred when frequency is tied. + +### 3. Whitespace Issues + +Leading or trailing spaces in any Option Name or Option Value cell. These are invisible in spreadsheets but cause Shopify to treat "Black" and "Black " as different values, creating phantom variants. + +### 4. Size Sequence Ordering + +Variants should appear in logical size order within each product. Check whether variant rows follow the expected sequence: + +- **Apparel letter sizes:** XS, S, M, L, XL, XXL, 2XL, 3XL +- **Numeric sizes:** ascending order (28, 30, 32, 34...) +- **Named sizes (e.g., Small, Medium, Large):** map to their letter equivalents first + +Flag products where size-based variants are out of sequence. Note: this check only applies to Option columns that contain size values. + +### 5. Duplicate Variants + +Two or more rows on the same product handle with identical Option1 + Option2 + Option3 value combinations. Compare after normalizing case and trimming whitespace to catch duplicates hidden by inconsistencies. + +If duplicates have different prices or inventory quantities, flag them for manual review rather than auto-merging. + +### 6. Missing Variant Images + +The `Variant Image` column is empty on a variant row while other variants on the same product have images. This check only applies when the input CSV includes the Variant Image column. + +### 7. Option Name Inconsistencies + +The same dimension called different things across products: Size vs Dimensions, Color vs Colour, Material vs Fabric. Compare all Option1/Option2/Option3 Name values across the file. Flag any near-matches. + +### 8. Handle/Title Drift + +The product handle should be a slugified version of the product title. Flag cases where the handle does not match what the title would produce (lowercase, hyphens for spaces, no special characters). Minor differences (e.g., dropping "Men's" or "Women's" from the handle) are acceptable and should not be flagged. + +--- + +## Output Structure + +### Output 1: Corrected CSV + +- Same columns as input, in the same order. +- All non-option columns preserved exactly (SKUs, prices, inventory, images, SEO fields, Google Shopping fields). +- Only Option Name cells, Option Value cells, and row ordering are modified. +- Offer as a downloadable file. + +### Output 2: Change Log + +```markdown +# Variant Normalization Change Log + +**Source file:** [original filename or "pasted CSV"] +**Products scanned:** N +**Variants scanned:** N +**Total changes made:** N + +--- + +## Canonical Option Values Established + +| Original Value | Canonical Value | Products Affected | Reason | +|---|---|---|---| + +## Issues Fixed + +### Option Value Aliases Merged +[Details per merge with affected products] + +### Case Normalized +[Details per value with affected products] + +### Whitespace Removed +[Details per field with affected products] + +### Size Order Corrected +| Product Handle | Previous Order | Corrected Order | +|---|---|---| + +### Duplicate Variants Flagged +| Product Handle | Option Combination | SKUs | Action Taken | +|---|---|---|---| + +### Missing Variant Images Flagged +| Product Handle | SKU | Option Values | Note | +|---|---|---|---| + +### Option Names Standardized +[Details per name change with affected products] + +### Handle/Title Drift Flagged +| Handle | Title | Recommendation | +|---|---|---| + +--- + +## Skipped / Needs Review + +[Anything that could not be resolved automatically, with explanation] +``` + +Only include sections where changes were made. Skip clean sections. + +--- + +## Edge Cases + +### Partial CSV upload + +The merchant may export only a subset of products. Normalize within the provided set. Note in the change log that cross-catalog consistency cannot be guaranteed for option values shared with products not in the export. + +### Non-apparel size systems + +Numeric shoe sizes, waist/inseam combinations (32x30), bottle volumes, equipment dimensions. Do not force the apparel letter-size ladder on these. Use ascending numeric order. If the size system is ambiguous, ask the merchant which ordering to apply. + +### Ambiguous aliases + +When two values might be the same thing (Slate vs Slate Grey) but could be intentionally distinct colors, flag for human review. Do not merge unless the merchant confirms. The audit report should present both values and ask. + +### Duplicate variants with different prices + +Flag but do not merge. The merchant must decide which price is correct. Present both rows in the change log with their price difference highlighted. + +### No Variant Image column + +Some exports do not include this column. If absent, skip the missing-image check and note it in the change log. + +### Very large CSV (500+ rows) + +Process all rows. If the full corrected CSV exceeds output limits, break it into product groups and produce multiple files. Note any grouping in the change log. + +### Matrixify or other export formats + +This skill expects Shopify's native product export CSV format. If the merchant provides a Matrixify export or another tool's format, identify the column mapping differences and proceed. Note the format in the change log. + +### Products with Option3 + +Some products use all three option slots (e.g., Size + Color + Material). The skill handles up to three option columns. Apply all detection categories to each option column independently. + +### Single-product CSV + +The skill works with a single product. Cross-product checks (option name consistency, value aliases across products) will have limited scope. Note this in the change log. + +--- + +## Closing + +Once the merchant approves the corrected CSV, note that it is ready for Shopify bulk import via Settings > Import in the Shopify admin. If variants were reordered, remind them that the import will update variant display order on the storefront. + +Suggest running this skill again after the next supplier data import or seasonal catalog update to catch new inconsistencies before they reach customers. diff --git a/skills/variant-option-normalizer/references/example-output.md b/skills/variant-option-normalizer/references/example-output.md new file mode 100644 index 0000000..d6a32a3 --- /dev/null +++ b/skills/variant-option-normalizer/references/example-output.md @@ -0,0 +1,192 @@ +# Example Output: SummitGear Co. Variant Normalization + +This example demonstrates the full output of the Normalize and Repair Variant Options skill using fictional data from SummitGear Co., an outdoor apparel brand. + +--- + +## Turn 1: Audit Report + +### Variant Option Audit + +**Products scanned:** 4 +**Variants scanned:** 22 +**Issues found:** 11 + +#### 1. Option Value Aliases + +| Values Found | Products Affected | Suggested Canonical Value | +|---|---|---| +| Grey, Gray | alpine-wind-jacket, ridgeline-fleece | Gray (US English standard) | +| XL, Extra Large | ridgeline-fleece | XL | + +#### 2. Case Inconsistencies + +| Values Found | Suggested Canonical Value | Products Affected | +|---|---|---| +| NAVY, Navy, navy | Navy | alpine-wind-jacket, basecamp-tee | + +#### 3. Whitespace Issues + +| Field | Value | Product Handle | +|---|---|---| +| Option2 Value | "Stone " (trailing space) | basecamp-tee | +| Option1 Value | " M" (leading space) | ridgeline-fleece | + +#### 4. Size Sequence Issues + +| Product Handle | Current Order | Expected Order | +|---|---|---| +| basecamp-tee | L, S, M, XL | S, M, L, XL | + +#### 5. Duplicate Variants + +| Product Handle | Option Combination | SKUs | Note | +|---|---|---|---| +| alpine-wind-jacket | M / Navy | SG-101-M-NAV, SG-101-M-NAV-2 | Same options, same price ($139.00) | + +#### 6. Missing Variant Images + +| Product Handle | SKU | Option Values | +|---|---|---| +| ridgeline-fleece | SG-102-L-GRY | L / Grey | +| ridgeline-fleece | SG-102-XL-GRY | XL / Grey | + +#### 7. Option Name Inconsistencies + +| Option Names Found | Products Using Each | Suggested Canonical Name | +|---|---|---| +| Color (3 products), Colour (1 product) | Colour: trail-shorts. Color: all others | Color | + +#### 8. Handle/Title Drift + +No issues found. + +--- + +## Turn 2: Normalization Plan + +Based on the audit, here is the proposed normalization plan: + +**Canonical value mapping:** + +| Original Value | Canonical Value | Reason | +|---|---|---| +| Grey | Gray | US English standard | +| Extra Large | XL | Consistent with other products | +| NAVY | Navy | Title case standard | +| navy | Navy | Title case standard | +| "Stone " | "Stone" | Trailing whitespace removed | +| " M" | "M" | Leading whitespace removed | +| Colour | Color | Matches 3 of 4 products | + +**Choices needed from you:** + +1. **Grey vs Gray:** I defaulted to "Gray" (US English). Would you prefer "Grey" (UK English)? +2. **Duplicate M/Navy on alpine-wind-jacket:** Both rows have the same price ($139.00). Should I remove the second row (SG-101-M-NAV-2) or keep both? + +**Size reordering:** +- basecamp-tee: L, S, M, XL will become S, M, L, XL + +--- + +## Turn 3: Output + +*Assuming the merchant confirmed defaults and chose to remove the duplicate.* + +### Corrected CSV + +```csv +Handle,Title,Body (HTML),Vendor,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Variant SKU,Variant Price,Variant Image +alpine-wind-jacket,Alpine Wind Jacket,

Lightweight wind protection.

,SummitGear Co.,Size,S,Color,Gray,SG-101-S-GRY,139.00,https://cdn.summitgear.co/alpine-wind-s-gray.jpg +alpine-wind-jacket,,,,Size,S,Color,Navy,SG-101-S-NAV,139.00,https://cdn.summitgear.co/alpine-wind-s-navy.jpg +alpine-wind-jacket,,,,Size,M,Color,Gray,SG-101-M-GRY,139.00,https://cdn.summitgear.co/alpine-wind-m-gray.jpg +alpine-wind-jacket,,,,Size,M,Color,Navy,SG-101-M-NAV,139.00,https://cdn.summitgear.co/alpine-wind-m-navy.jpg +alpine-wind-jacket,,,,Size,L,Color,Gray,SG-101-L-GRY,139.00,https://cdn.summitgear.co/alpine-wind-l-gray.jpg +alpine-wind-jacket,,,,Size,L,Color,Navy,SG-101-L-NAV,139.00,https://cdn.summitgear.co/alpine-wind-l-navy.jpg +ridgeline-fleece,Ridgeline Fleece,

Midweight fleece pullover.

,SummitGear Co.,Size,S,Color,Gray,SG-102-S-GRY,89.00,https://cdn.summitgear.co/ridgeline-s-gray.jpg +ridgeline-fleece,,,,Size,M,Color,Gray,SG-102-M-GRY,89.00,https://cdn.summitgear.co/ridgeline-m-gray.jpg +ridgeline-fleece,,,,Size,L,Color,Gray,SG-102-L-GRY,89.00, +ridgeline-fleece,,,,Size,XL,Color,Gray,SG-102-XL-GRY,89.00, +basecamp-tee,Basecamp Tee,

Everyday trail tee.

,SummitGear Co.,Size,S,Color,Navy,SG-103-S-NAV,35.00,https://cdn.summitgear.co/basecamp-s-navy.jpg +basecamp-tee,,,,Size,S,Color,Stone,SG-103-S-STN,35.00,https://cdn.summitgear.co/basecamp-s-stone.jpg +basecamp-tee,,,,Size,M,Color,Navy,SG-103-M-NAV,35.00,https://cdn.summitgear.co/basecamp-m-navy.jpg +basecamp-tee,,,,Size,M,Color,Stone,SG-103-M-STN,35.00,https://cdn.summitgear.co/basecamp-m-stone.jpg +basecamp-tee,,,,Size,L,Color,Navy,SG-103-L-NAV,35.00,https://cdn.summitgear.co/basecamp-l-navy.jpg +basecamp-tee,,,,Size,L,Color,Stone,SG-103-L-STN,35.00,https://cdn.summitgear.co/basecamp-l-stone.jpg +basecamp-tee,,,,Size,XL,Color,Navy,SG-103-XL-NAV,35.00,https://cdn.summitgear.co/basecamp-xl-navy.jpg +basecamp-tee,,,,Size,XL,Color,Stone,SG-103-XL-STN,35.00,https://cdn.summitgear.co/basecamp-xl-stone.jpg +trail-shorts,Trail Shorts,

Quick-dry hiking shorts.

,SummitGear Co.,Size,M,Color,Olive,SG-104-M-OLV,55.00,https://cdn.summitgear.co/trail-shorts-m-olive.jpg +trail-shorts,,,,Size,L,Color,Olive,SG-104-L-OLV,55.00,https://cdn.summitgear.co/trail-shorts-l-olive.jpg +trail-shorts,,,,Size,XL,Color,Olive,SG-104-XL-OLV,55.00,https://cdn.summitgear.co/trail-shorts-xl-olive.jpg +``` + +*Note: The above CSV is simplified for readability. The actual output preserves all 51 columns from the original Shopify export.* + +### Change Log + +```markdown +# Variant Normalization Change Log + +**Source file:** summitgear-products.csv +**Products scanned:** 4 +**Variants scanned:** 22 +**Total changes made:** 11 + +--- + +## Canonical Option Values Established + +| Original Value | Canonical Value | Products Affected | Reason | +|---|---|---|---| +| Grey | Gray | alpine-wind-jacket, ridgeline-fleece | US English standard (merchant confirmed) | +| Extra Large | XL | ridgeline-fleece | Consistent with abbreviated form used across catalog | +| NAVY | Navy | alpine-wind-jacket | Title case normalization | +| navy | Navy | basecamp-tee | Title case normalization | +| Colour | Color | trail-shorts | Matches 3 of 4 products in catalog | + +## Issues Fixed + +### Option Value Aliases Merged + +- **Grey to Gray:** 4 variants across alpine-wind-jacket and ridgeline-fleece updated. All instances of "Grey" replaced with "Gray." +- **Extra Large to XL:** 2 variants on ridgeline-fleece updated. "Extra Large" replaced with "XL" to match catalog standard. + +### Case Normalized + +- **NAVY to Navy:** 2 variants on alpine-wind-jacket updated. +- **navy to Navy:** 2 variants on basecamp-tee updated. + +### Whitespace Removed + +- **"Stone " to "Stone":** 4 variants on basecamp-tee. Trailing space removed. +- **" M" to "M":** 1 variant on ridgeline-fleece. Leading space removed. + +### Size Order Corrected + +| Product Handle | Previous Order | Corrected Order | +|---|---|---| +| basecamp-tee | L, S, M, XL | S, M, L, XL | + +### Duplicate Variants Flagged + +| Product Handle | Option Combination | SKUs | Action Taken | +|---|---|---|---| +| alpine-wind-jacket | M / Navy | SG-101-M-NAV, SG-101-M-NAV-2 | Removed SG-101-M-NAV-2 (merchant confirmed) | + +### Missing Variant Images Flagged + +| Product Handle | SKU | Option Values | Note | +|---|---|---|---| +| ridgeline-fleece | SG-102-L-GRY | L / Gray | No variant image. Other variants on this product have images. | +| ridgeline-fleece | SG-102-XL-GRY | XL / Gray | No variant image. Other variants on this product have images. | + +### Option Names Standardized + +- **Colour to Color:** All variants on trail-shorts updated. "Colour" replaced with "Color" to match the other 3 products in the file. + +--- + +## Skipped / Needs Review + +- **Missing variant images on ridgeline-fleece:** The skill cannot generate image URLs. The merchant should upload images for L/Gray and XL/Gray variants and add the URLs to the Variant Image column before importing. +``` diff --git a/skills/variant-option-normalizer/skillshelf.yaml b/skills/variant-option-normalizer/skillshelf.yaml new file mode 100644 index 0000000..e024856 --- /dev/null +++ b/skills/variant-option-normalizer/skillshelf.yaml @@ -0,0 +1,58 @@ +version: "1.0" +category: catalog-operations +subcategories: + - variants + - data-quality + - shopify +level: intermediate +primitive: false +platforms: + - shopify +tags: + - variants + - normalization + - data-cleanup + - product-csv + - catalog-operations + - bulk-edit + +date_added: "2026-04-05" +date_updated: "2026-04-05" + +author: + name: Raqib Abdullah + url: https://github.com/raqibdev + +input_schema: + required: + - Shopify product export CSV + recommended: + - Brand style guide for naming conventions + +output_schema: + format: Two files + sections: + - Corrected product CSV + - Change log (Markdown) + +faq: + - question: What does this skill do? + answer: >- + It scans a Shopify product CSV for inconsistent option values (color + aliases, size label variants, case issues), fixes them, reorders size + sequences, flags duplicates and missing images, and returns a corrected + CSV plus a change log. + - question: What input does it need? + answer: >- + A Shopify product export CSV. Paste rows directly or upload the file. + The skill works with partial exports (a subset of products). + - question: Will it change my SKUs or prices? + answer: >- + No. The skill only modifies Option Name and Option Value columns plus + variant row ordering. SKUs, prices, inventory, and all other fields are + preserved exactly. + - question: What if I disagree with a proposed canonical value? + answer: >- + The skill presents its normalization plan before writing output and asks + for confirmation on any ambiguous cases. You can override any proposed + canonical value before the corrected CSV is produced. From 47f7d56988e92b238cd5f464c91835ac920f76df Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Sun, 5 Apr 2026 01:50:58 +0600 Subject: [PATCH 02/24] Fix: MPN mismatch and add guidance for maintaining product-level metadata during row reordering in variant option normalizer --- .../shopify-products-messy-variants.csv | 88 +++++++++---------- skills/variant-option-normalizer/SKILL.md | 2 + 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv b/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv index 7a74f54..00c1546 100644 --- a/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv +++ b/fixtures/greatoutdoorsco/shopify-products-messy-variants.csv @@ -1,44 +1,44 @@ -Handle,Title,Body (HTML),Vendor,Product Category,Type,Tags,Published,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,Variant SKU,Variant Grams,Variant Inventory Tracker,Variant Inventory Qty,Variant Inventory Policy,Variant Fulfillment Service,Variant Price,Variant Compare At Price,Variant Requires Shipping,Variant Taxable,Variant Barcode,Image Src,Image Position,Image Alt Text,Gift Card,SEO Title,SEO Description,Google Shopping / Google Product Category,Google Shopping / Gender,Google Shopping / Age Group,Google Shopping / MPN,Google Shopping / AdWords Grouping,Google Shopping / AdWords Labels,Google Shopping / Condition,Google Shopping / Custom Product,Google Shopping / Custom Label 0,Google Shopping / Custom Label 1,Google Shopping / Custom Label 2,Google Shopping / Custom Label 3,Google Shopping / Custom Label 4,Variant Image,Variant Weight Unit,Variant Tax Code,Cost per item,Price / International,Compare At Price / International,Status -cascade-rain-shell-mens,Cascade Rain Shell - Men's,"

Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.

Weight: 330g. Material: 100% recycled nylon ripstop.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, hiking",TRUE,Size,S,Color,Slate,,,GOC-001-S-SLAT,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens.jpg,1,Cascade Rain Shell - Men's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Men's | Great Outdoors Co.,"Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.2.5-layer waterproof/breathable fabricHelmet-compa...",Apparel & Accessories > Clothing > Outerwear > Raincoats,male,adult,GOC-001-S-SLAT,Men's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,,active -cascade-rain-shell-mens,,,,,,,,Size,S,Color,Fir Green,,,GOC-001-S-FIRG,330,shopify,7,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-S-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,M,Color,slate,,,GOC-001-M-SLAT,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,M,Color,Fir Green ,,,GOC-001-M-FIRG,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,L,Color,SLATE,,,GOC-001-L-SLAT,330,shopify,4,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,L,Color,Fir Green,,,GOC-001-L-FIRG,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Slate,,,GOC-001-XL-SLAT,330,shopify,5,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, -cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Fir Green,,,GOC-001-XL-FIRG,330,shopify,8,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, -cascade-rain-shell-womens,Cascade Rain Shell - Women's,"

Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.

Waterproof rating: 15K. Weight: 310g. Material: 100% recycled nylon.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, trail",TRUE,Size,XS,Color,Deep Teal,,,GOC-002-XS-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens.jpg,1,Cascade Rain Shell - Women's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Women's | Great Outdoors Co.,"Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.Waterproof rating: 15K. Weight: 310g. Material: 100% recycled ...",Apparel & Accessories > Clothing > Outerwear > Raincoats,female,adult,GOC-002-XS-DEEP,Women's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,,active -cascade-rain-shell-womens,,,,,,,,Size,XS,Color,Cinder,,,GOC-002-XS-CIND,310,shopify,8,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XS-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP-DUP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,S,Color,Cinder,,,GOC-002-S-CIND,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,M,Color,Deep Teal,,,GOC-002-M-DEEP,310,shopify,6,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,M,Color,Cinder,,,GOC-002-M-CIND,310,shopify,10,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,L,Color,Deep Teal,,,GOC-002-L-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,L,Color,Cinder,,,GOC-002-L-CIND,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Deep Teal,,,GOC-002-XL-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, -cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Cinder,,,GOC-002-XL-CIND,310,shopify,5,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, -timberline-fleece-pullover-mens,Timberline Fleece Pullover - Men's,"

Midweight fleece for chilly starts & late-night campfires.

Soft, durable, and easy to layer under a shell.",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Fleece,"fleece, midlayer, camp, hiking, everyday",TRUE,Size,S,Color,Charcoal Heather ,,,GOC-003-S-CHAR,380,shopify,12,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens.jpg,1,Timberline Fleece Pullover - Men's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Men's | Great Outdoors Co.,"Midweight fleece for chilly starts & late-night campfires.Soft, durable, and easy to layer under a shell.",Apparel & Accessories > Clothing > Outerwear > Fleece,male,adult,GOC-003-S-CHAR,Men's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,,active -timberline-fleece-pullover-mens,,,,,,,,Size,S,Color,River Blue,,,GOC-003-S-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-S-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,Charcoal Heather,,,GOC-003-M-CHAR,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,River Blue,,,GOC-003-M-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,Charcoal Heather,,,GOC-003-L-CHAR,380,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,River Blue,,,GOC-003-L-RIVE,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,Charcoal Heather,,,GOC-003-XL-CHAR,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,River Blue,,,GOC-003-XL-RIVE,380,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,Timberline Fleece Pullover - Women's,

Midweight warmth that layers cleanly under a shell and looks right at home around town.

Weight: 360g. Material: 100% recycled polyester fleece.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Fleece,"fleece, midlayer, trail, campfire, cozy",TRUE,Size,L,Colour,Oat Heather,,,GOC-004-L-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens.jpg,1,Timberline Fleece Pullover - Women's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Women's | Great Outdoors Co.,Midweight warmth that layers cleanly under a shell and looks right at home around town.Weight: 360g. Material: 100% recycled polyester fleece.,Apparel & Accessories > Clothing > Outerwear > Fleece,female,adult,GOC-004-XS-OATH,Women's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,,active -timberline-fleece-pullover-womens,,,,,,,,Size,L,Colour,Evergreen,,,GOC-004-L-EVER,360,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-L-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Oat Heather,,,GOC-004-XS-OATH,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Evergreen,,,GOC-004-XS-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-XS-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Oat Heather,,,GOC-004-S-OATH,360,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Evergreen,,,GOC-004-S-EVER,360,shopify,11,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Oat Heather,,,GOC-004-M-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, -timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Evergreen,,,GOC-004-M-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, -evergreen-merino-base-top-mens,Evergreen Merino Base Top - Men's,

Soft next-to-skin merino that regulates temperature across seasons.

Weight: 220g. Material: 87% merino wool / 13% nylon.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Underwear & Socks,Men's Base Layer,"merino, base-layer, odor-resistant, hiking, ski",TRUE,Size,S,Color,Black,,,GOC-005-S-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens.jpg,1,Evergreen Merino Base Top - Men's - Great Outdoors Co.,FALSE,Evergreen Merino Base Top - Men's | Great Outdoors Co.,Soft next-to-skin merino that regulates temperature across seasons.Weight: 220g. Material: 87% merino wool / 13% nylon.,Apparel & Accessories > Clothing > Activewear,male,adult,GOC-005-S-BLAC,Men's Base Layer,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,,active -evergreen-merino-base-top-mens,,,,,,,,Size,S,Color,Heather Grey,,,GOC-005-S-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-S-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Black,,,GOC-005-M-BLAC,220,shopify,10,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-BLAC,,,,,,,,,,,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Heather Grey,,,GOC-005-M-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Black,,,GOC-005-L-BLAC,220,shopify,11,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Heather Gray,,,GOC-005-L-HEAT,220,shopify,12,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-HEAT,,,,,,,,,,,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Black,,,GOC-005-XL-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, -evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Heather Gray,,,GOC-005-XL-HEAT,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, +Handle,Title,Body (HTML),Vendor,Product Category,Type,Tags,Published,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,Variant SKU,Variant Grams,Variant Inventory Tracker,Variant Inventory Qty,Variant Inventory Policy,Variant Fulfillment Service,Variant Price,Variant Compare At Price,Variant Requires Shipping,Variant Taxable,Variant Barcode,Image Src,Image Position,Image Alt Text,Gift Card,SEO Title,SEO Description,Google Shopping / Google Product Category,Google Shopping / Gender,Google Shopping / Age Group,Google Shopping / MPN,Google Shopping / AdWords Grouping,Google Shopping / AdWords Labels,Google Shopping / Condition,Google Shopping / Custom Product,Google Shopping / Custom Label 0,Google Shopping / Custom Label 1,Google Shopping / Custom Label 2,Google Shopping / Custom Label 3,Google Shopping / Custom Label 4,Variant Image,Variant Weight Unit,Variant Tax Code,Cost per item,Price / International,Compare At Price / International,Status +cascade-rain-shell-mens,Cascade Rain Shell - Men's,"

Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.

Weight: 330g. Material: 100% recycled nylon ripstop.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, hiking",TRUE,Size,S,Color,Slate,,,GOC-001-S-SLAT,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens.jpg,1,Cascade Rain Shell - Men's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Men's | Great Outdoors Co.,"Built for misty mornings and surprise squalls, the Cascade Rain Shell keeps you dry without feeling clammy.2.5-layer waterproof/breathable fabricHelmet-compa...",Apparel & Accessories > Clothing > Outerwear > Raincoats,male,adult,GOC-001-S-SLAT,Men's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,,active +cascade-rain-shell-mens,,,,,,,,Size,S,Color,Fir Green,,,GOC-001-S-FIRG,330,shopify,7,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-S-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,M,Color,slate,,,GOC-001-M-SLAT,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,M,Color,Fir Green ,,,GOC-001-M-FIRG,330,shopify,10,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-M-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,L,Color,SLATE,,,GOC-001-L-SLAT,330,shopify,4,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,L,Color,Fir Green,,,GOC-001-L-FIRG,330,shopify,9,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-L-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Slate,,,GOC-001-XL-SLAT,330,shopify,5,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-SLAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-slate.jpg,g,,81.95,,, +cascade-rain-shell-mens,,,,,,,,Size,XL,Color,Fir Green,,,GOC-001-XL-FIRG,330,shopify,8,deny,manual,149.00,,TRUE,TRUE,,,,,,,,,,,GOC-001-XL-FIRG,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-mens-fir-green.jpg,g,,81.95,,, +cascade-rain-shell-womens,Cascade Rain Shell - Women's,"

Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.

Waterproof rating: 15K. Weight: 310g. Material: 100% recycled nylon.

",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Rain Jacket,"rainwear, waterproof, breathable, pacific-northwest, trail",TRUE,Size,XS,Color,Deep Teal,,,GOC-002-XS-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens.jpg,1,Cascade Rain Shell - Women's - Great Outdoors Co.,FALSE,Cascade Rain Shell - Women's | Great Outdoors Co.,"Our do-it-all rain shell for the Pacific Northwest: light, quiet, and ready for everyday miles.Waterproof rating: 15K. Weight: 310g. Material: 100% recycled ...",Apparel & Accessories > Clothing > Outerwear > Raincoats,female,adult,GOC-002-XS-DEEP,Women's Rain Jacket,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,,active +cascade-rain-shell-womens,,,,,,,,Size,XS,Color,Cinder,,,GOC-002-XS-CIND,310,shopify,8,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XS-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Deep Teal,,,GOC-002-S-DEEP-DUP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,S,Color,Cinder,,,GOC-002-S-CIND,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-S-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,M,Color,Deep Teal,,,GOC-002-M-DEEP,310,shopify,6,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,M,Color,Cinder,,,GOC-002-M-CIND,310,shopify,10,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-M-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,L,Color,Deep Teal,,,GOC-002-L-DEEP,310,shopify,7,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,L,Color,Cinder,,,GOC-002-L-CIND,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-L-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Deep Teal,,,GOC-002-XL-DEEP,310,shopify,4,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-DEEP,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-deep-teal.jpg,g,,81.95,,, +cascade-rain-shell-womens,,,,,,,,Size,XL,Color,Cinder,,,GOC-002-XL-CIND,310,shopify,5,deny,manual,149.00,179.00,TRUE,TRUE,,,,,,,,,,,GOC-002-XL-CIND,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/cascade-rain-shell-womens-cinder.jpg,g,,81.95,,, +timberline-fleece-pullover-mens,Timberline Fleece Pullover - Men's,"

Midweight fleece for chilly starts & late-night campfires.

Soft, durable, and easy to layer under a shell.",Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Men's Fleece,"fleece, midlayer, camp, hiking, everyday",TRUE,Size,S,Color,Charcoal Heather ,,,GOC-003-S-CHAR,380,shopify,12,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens.jpg,1,Timberline Fleece Pullover - Men's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Men's | Great Outdoors Co.,"Midweight fleece for chilly starts & late-night campfires.Soft, durable, and easy to layer under a shell.",Apparel & Accessories > Clothing > Outerwear > Fleece,male,adult,GOC-003-S-CHAR,Men's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,,active +timberline-fleece-pullover-mens,,,,,,,,Size,S,Color,River Blue,,,GOC-003-S-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-S-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,Charcoal Heather,,,GOC-003-M-CHAR,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,M,Color,River Blue,,,GOC-003-M-RIVE,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-M-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,Charcoal Heather,,,GOC-003-L-CHAR,380,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,L,Color,River Blue,,,GOC-003-L-RIVE,380,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-L-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,Charcoal Heather,,,GOC-003-XL-CHAR,380,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-CHAR,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-charcoal-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-mens,,,,,,,,Size,Extra Large,Color,River Blue,,,GOC-003-XL-RIVE,380,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-003-XL-RIVE,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-mens-river-blue.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,Timberline Fleece Pullover - Women's,

Midweight warmth that layers cleanly under a shell and looks right at home around town.

Weight: 360g. Material: 100% recycled polyester fleece.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Outerwear,Women's Fleece,"fleece, midlayer, trail, campfire, cozy",TRUE,Size,L,Colour,Oat Heather,,,GOC-004-L-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens.jpg,1,Timberline Fleece Pullover - Women's - Great Outdoors Co.,FALSE,Timberline Fleece Pullover - Women's | Great Outdoors Co.,Midweight warmth that layers cleanly under a shell and looks right at home around town.Weight: 360g. Material: 100% recycled polyester fleece.,Apparel & Accessories > Clothing > Outerwear > Fleece,female,adult,GOC-004-L-OATH,Women's Fleece,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,,active +timberline-fleece-pullover-womens,,,,,,,,Size,L,Colour,Evergreen,,,GOC-004-L-EVER,360,shopify,10,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-L-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Oat Heather,,,GOC-004-XS-OATH,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-XS-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,XS,Colour,Evergreen,,,GOC-004-XS-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-XS-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Oat Heather,,,GOC-004-S-OATH,360,shopify,9,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,S,Colour,Evergreen,,,GOC-004-S-EVER,360,shopify,11,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-S-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Oat Heather,,,GOC-004-M-OATH,360,shopify,13,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-OATH,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-oat-heather.jpg,g,,48.95,,, +timberline-fleece-pullover-womens,,,,,,,,Size,M,Colour,Evergreen,,,GOC-004-M-EVER,360,shopify,8,deny,manual,89.00,,TRUE,TRUE,,,,,,,,,,,GOC-004-M-EVER,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/timberline-fleece-pullover-womens-evergreen.jpg,g,,48.95,,, +evergreen-merino-base-top-mens,Evergreen Merino Base Top - Men's,

Soft next-to-skin merino that regulates temperature across seasons.

Weight: 220g. Material: 87% merino wool / 13% nylon.

,Great Outdoors Co.,Apparel & Accessories > Clothing > Underwear & Socks,Men's Base Layer,"merino, base-layer, odor-resistant, hiking, ski",TRUE,Size,S,Color,Black,,,GOC-005-S-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens.jpg,1,Evergreen Merino Base Top - Men's - Great Outdoors Co.,FALSE,Evergreen Merino Base Top - Men's | Great Outdoors Co.,Soft next-to-skin merino that regulates temperature across seasons.Weight: 220g. Material: 87% merino wool / 13% nylon.,Apparel & Accessories > Clothing > Activewear,male,adult,GOC-005-S-BLAC,Men's Base Layer,pnw,new,FALSE,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,,active +evergreen-merino-base-top-mens,,,,,,,,Size,S,Color,Heather Grey,,,GOC-005-S-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-S-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Black,,,GOC-005-M-BLAC,220,shopify,10,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-BLAC,,,,,,,,,,,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,M,Color,Heather Grey,,,GOC-005-M-HEAT,220,shopify,9,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-M-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Black,,,GOC-005-L-BLAC,220,shopify,11,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,L,Color,Heather Gray,,,GOC-005-L-HEAT,220,shopify,12,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-L-HEAT,,,,,,,,,,,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Black,,,GOC-005-XL-BLAC,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-BLAC,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-black.jpg,g,,42.90,,, +evergreen-merino-base-top-mens,,,,,,,,Size,XL,Color,Heather Gray,,,GOC-005-XL-HEAT,220,shopify,15,deny,manual,78.00,,TRUE,TRUE,,,,,,,,,,,GOC-005-XL-HEAT,,,,,,,,,,https://cdn.greatoutdoorsco.com/images/evergreen-merino-base-top-mens-heather-gray.jpg,g,,42.90,,, diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 7a8e977..a032fa1 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -132,6 +132,8 @@ Variants should appear in logical size order within each product. Check whether Flag products where size-based variants are out of sequence. Note: this check only applies to Option columns that contain size values. +When reordering rows, keep product-level metadata (Title, Body, Vendor, Tags, Image Src, SEO fields, Published, Status) on the first row of each product handle group. If a row moves into the first position, transfer those fields to it and clear them from the displaced row. + ### 5. Duplicate Variants Two or more rows on the same product handle with identical Option1 + Option2 + Option3 value combinations. Compare after normalizing case and trimming whitespace to catch duplicates hidden by inconsistencies. From 9c0b568ceba4dfe1aa659bc69dbd6c20798ea899 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Sun, 5 Apr 2026 02:05:15 +0600 Subject: [PATCH 03/24] feat: Enhance variant option normalization guidance by specifying whitespace trimming, clarifying missing image checks, and adding reminders for Shopify import processes. --- skills/variant-option-normalizer/SKILL.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index a032fa1..e05f657 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -112,7 +112,7 @@ Same color, size, or material referred to by different strings across variants o - Size: XL/Extra Large/X-Large, S/Small, M/Medium - Material: variations in fiber content descriptions -Compare values using normalized lowercase forms. Flag any pair of values within the same option name where one could be an alias of the other. +Compare values using normalized lowercase, whitespace-trimmed forms. Flag any pair of values within the same option name where one could be an alias of the other. When two values appear on the same product, note that explicitly (same-product aliases are almost certainly errors; cross-product aliases may be intentional). ### 2. Case Inconsistencies @@ -142,7 +142,7 @@ If duplicates have different prices or inventory quantities, flag them for manua ### 6. Missing Variant Images -The `Variant Image` column is empty on a variant row while other variants on the same product have images. This check only applies when the input CSV includes the Variant Image column. +The `Variant Image` column is empty on a variant row while other variants on the same product have images. This check only applies when the input CSV includes the Variant Image column. Note: this check detects missing URLs, not broken URLs. Populated image URLs are not validated. ### 7. Option Name Inconsistencies @@ -150,7 +150,7 @@ The same dimension called different things across products: Size vs Dimensions, ### 8. Handle/Title Drift -The product handle should be a slugified version of the product title. Flag cases where the handle does not match what the title would produce (lowercase, hyphens for spaces, no special characters). Minor differences (e.g., dropping "Men's" or "Women's" from the handle) are acceptable and should not be flagged. +The product handle should be a slugified version of the product title. Flag cases where the handle does not match what the title would produce (lowercase, hyphens for spaces, apostrophes removed, consecutive hyphens collapsed, no other special characters). Minor differences (e.g., dropping "Men's" or "Women's" from the handle) are acceptable and should not be flagged. --- @@ -158,7 +158,7 @@ The product handle should be a slugified version of the product title. Flag case ### Output 1: Corrected CSV -- Same columns as input, in the same order. +- Same columns as input, in the same order. Copy every non-option cell verbatim. Do not parse, reformat, or truncate HTML in the Body column. Do not drop columns, including Google Shopping fields, metafield columns, or any other columns present in the input. - All non-option columns preserved exactly (SKUs, prices, inventory, images, SEO fields, Google Shopping fields). - Only Option Name cells, Option Value cells, and row ordering are modified. - Offer as a downloadable file. @@ -263,6 +263,10 @@ The skill works with a single product. Cross-product checks (option name consist ## Closing -Once the merchant approves the corrected CSV, note that it is ready for Shopify bulk import via Settings > Import in the Shopify admin. If variants were reordered, remind them that the import will update variant display order on the storefront. +Once the merchant approves the corrected CSV, note that it is ready for Shopify bulk import via Settings > Import in the Shopify admin. Include these reminders: + +- If variants were reordered, the import will update variant display order on the storefront. +- Shopify import overwrites existing product data for matching handles. Back up the current export before importing. +- Shopify import cannot delete variants, only add or update them. If a duplicate row was removed from the CSV, the merchant must also delete that variant manually in the admin. Suggest running this skill again after the next supplier data import or seasonal catalog update to catch new inconsistencies before they reach customers. From 472a6dbd6e5a2966be7f25501126b06183f55c93 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Sun, 5 Apr 2026 02:56:32 +0600 Subject: [PATCH 04/24] feat: Add prompts for merchant file saving locations and filenames for corrected CSV and change log outputs --- skills/variant-option-normalizer/SKILL.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index e05f657..a0bbc2a 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -92,6 +92,13 @@ Generate two outputs: 1. **Corrected CSV** as a downloadable file. Same column structure as the input. All header columns preserved. Changes applied inline. 2. **Change log** as a Markdown document using the output structure below. +After generating both outputs, ask the merchant: + +- Where to save the corrected CSV (suggest a filename based on the input, e.g. `shopify-products-normalized.csv` in the same directory). +- Where to save the change log (suggest a filename alongside the CSV, e.g. `shopify-products-normalized-changelog.md`). + +Write both files to the confirmed paths before closing out the turn. + Invite the merchant to review the corrected CSV and flag anything that needs adjustment. ### Turn 4+: Revise From 2b7d97cc01abf9d36c90379482fa1ac8a6a4d0fd Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Sun, 5 Apr 2026 03:00:35 +0600 Subject: [PATCH 05/24] feat: Clarify instructions for merchant CSV submission in variant option normalizer --- skills/variant-option-normalizer/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index a0bbc2a..3225113 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -26,7 +26,9 @@ You are a catalog operations specialist helping an ecommerce team clean up varia ### Turn 1: Scan and Audit -Ask the merchant to provide their Shopify product CSV. They can paste rows directly or upload the file. Accept whatever they provide. +Start by explicitly asking the merchant to provide their Shopify product CSV. Do not assume, guess, or look for an existing file. Wait for the merchant to share it before proceeding. + +Example prompt: "Please share your Shopify product CSV. You can paste the rows directly or provide the file path." Once you have the CSV: From 1ebe6bba2e27b3848db9ad43d8cd367e825124dd Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 22:21:55 +0600 Subject: [PATCH 06/24] feat: Add option name and size aliases JSON files for variant option normalization --- .../assets/option_name_aliases.json | 26 ++++++++++++++ .../assets/size_aliases.json | 36 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 skills/variant-option-normalizer/assets/option_name_aliases.json create mode 100644 skills/variant-option-normalizer/assets/size_aliases.json diff --git a/skills/variant-option-normalizer/assets/option_name_aliases.json b/skills/variant-option-normalizer/assets/option_name_aliases.json new file mode 100644 index 0000000..6f83097 --- /dev/null +++ b/skills/variant-option-normalizer/assets/option_name_aliases.json @@ -0,0 +1,26 @@ +{ + "option_name_synonyms": [ + ["Color", "Colour"], + ["Size", "Sizes", "Dimension", "Dimensions"], + ["Material", "Fabric", "Composition"], + ["Style", "Design"], + ["Pattern", "Print"], + ["Flavor", "Flavour"], + ["Scent", "Fragrance"], + ["Length", "Inseam"], + ["Width", "Waist"], + ["Weight", "Mass"], + ["Volume", "Capacity"] + ], + "color_aliases": { + "grey": ["gray"], + "charcoal heather": ["charcoal"], + "navy blue": ["navy"], + "heather grey": ["heather gray"], + "burgundy": ["wine"], + "cream": ["ivory", "off-white", "offwhite"], + "tan": ["khaki", "beige"], + "fuchsia": ["hot pink"], + "magenta": ["hot pink"] + } +} diff --git a/skills/variant-option-normalizer/assets/size_aliases.json b/skills/variant-option-normalizer/assets/size_aliases.json new file mode 100644 index 0000000..28a8112 --- /dev/null +++ b/skills/variant-option-normalizer/assets/size_aliases.json @@ -0,0 +1,36 @@ +{ + "apparel_letter_order": [ + "XXS", "XS", "S", "M", "L", "XL", "XXL", "2XL", "3XL", "4XL", "5XL" + ], + "aliases": { + "extra extra small": "XXS", + "extra small": "XS", + "x-small": "XS", + "xs": "XS", + "small": "S", + "sm": "S", + "medium": "M", + "med": "M", + "large": "L", + "lg": "L", + "extra large": "XL", + "x-large": "XL", + "extra-large": "XL", + "xx-large": "XXL", + "2x-large": "2XL", + "2xl": "2XL", + "3x-large": "3XL", + "3xl": "3XL", + "4x-large": "4XL", + "4xl": "4XL", + "5x-large": "5XL", + "5xl": "5XL", + "one size": "OS", + "o/s": "OS", + "os": "OS", + "onesize": "OS" + }, + "size_option_names": [ + "size", "sizes", "dimension", "dimensions" + ] +} From 47bda3e802d5834633e1429d08e8ce056b6e5c97 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 22:32:52 +0600 Subject: [PATCH 07/24] feat: Implement variant option audit script for Shopify product CSVs --- .../scripts/normalize_audit.py | 1089 +++++++++++++++++ 1 file changed, 1089 insertions(+) create mode 100644 skills/variant-option-normalizer/scripts/normalize_audit.py diff --git a/skills/variant-option-normalizer/scripts/normalize_audit.py b/skills/variant-option-normalizer/scripts/normalize_audit.py new file mode 100644 index 0000000..bc22a1c --- /dev/null +++ b/skills/variant-option-normalizer/scripts/normalize_audit.py @@ -0,0 +1,1089 @@ +#!/usr/bin/env python3 +"""Audit a Shopify product CSV for variant option inconsistencies. + +Outputs structured JSON to stdout. Run from the skill directory: + python3 scripts/normalize_audit.py [--assets-dir assets/] + +Exit codes: + 0 - Audit completed (issues may or may not exist) + 1 - Fatal error (file not found, parse failure) +""" + +import argparse +import csv +import html +import json +import re +import sys +import unicodedata +from collections import defaultdict +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +SCRIPT_VERSION = "1.0.0" + +# Shopify caps option values at 255 characters +SHOPIFY_OPTION_VALUE_MAX_LENGTH = 255 + +# Unicode whitespace beyond standard ASCII space/tab/newline +ABNORMAL_WHITESPACE = { + "\u00a0": "non-breaking space", + "\u200b": "zero-width space", + "\u200c": "zero-width non-joiner", + "\u200d": "zero-width joiner", + "\ufeff": "byte order mark", + "\u2003": "em space", + "\u2002": "en space", + "\u2009": "thin space", + "\u200a": "hair space", + "\u3000": "ideographic space", + "\u2007": "figure space", + "\u2008": "punctuation space", + "\u205f": "medium mathematical space", +} + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- + +@dataclass +class VariantRow: + row_number: int + handle: str + title: str + option1_name: str + option1_value: str + option2_name: str + option2_value: str + option3_name: str + option3_value: str + variant_sku: str + variant_price: str + variant_inventory_qty: str + variant_image: str + raw_row: dict + + +@dataclass +class Product: + handle: str + title: str + rows: list[VariantRow] = field(default_factory=list) + + +# --------------------------------------------------------------------------- +# CSV parsing +# --------------------------------------------------------------------------- + +def detect_encoding_and_bom(file_path: Path) -> tuple[str, bool]: + """Check for UTF-8 BOM. Returns (encoding, bom_detected).""" + with open(file_path, "rb") as f: + raw = f.read(4) + if raw[:3] == b"\xef\xbb\xbf": + return "utf-8-sig", True + return "utf-8-sig", False # utf-8-sig strips BOM if present, safe for both + + +def parse_csv(file_path: Path) -> tuple[list[Product], list[str], dict]: + """Parse a Shopify product CSV into Product groups. + + Returns (products, column_names, metadata_dict). + """ + encoding, bom_detected = detect_encoding_and_bom(file_path) + + try: + with open(file_path, newline="", encoding=encoding) as f: + reader = csv.DictReader(f) + columns = reader.fieldnames or [] + rows_raw = list(reader) + except UnicodeDecodeError: + # Fall back to latin-1 which accepts any byte + with open(file_path, newline="", encoding="latin-1") as f: + reader = csv.DictReader(f) + columns = reader.fieldnames or [] + rows_raw = list(reader) + encoding = "latin-1" + + if not columns: + print("Fatal: CSV has no columns.", file=sys.stderr) + sys.exit(1) + + def get(row: dict, key: str) -> str: + return row.get(key, "") or "" + + products_map: dict[str, Product] = {} + product_order: list[str] = [] + total_variants = 0 + last_handle = "" + + for idx, row in enumerate(rows_raw): + row_number = idx + 2 # 1-based, header is row 1 + handle = get(row, "Handle").strip() + + # Variant-only rows inherit the handle from the previous product row + if not handle: + handle = last_handle + else: + last_handle = handle + + if not handle: + continue # skip rows with no handle at all + + title = get(row, "Title") + + vr = VariantRow( + row_number=row_number, + handle=handle, + title=title, + option1_name=get(row, "Option1 Name"), + option1_value=get(row, "Option1 Value"), + option2_name=get(row, "Option2 Name"), + option2_value=get(row, "Option2 Value"), + option3_name=get(row, "Option3 Name"), + option3_value=get(row, "Option3 Value"), + variant_sku=get(row, "Variant SKU"), + variant_price=get(row, "Variant Price"), + variant_inventory_qty=get(row, "Variant Inventory Qty"), + variant_image=get(row, "Variant Image"), + raw_row=row, + ) + + if handle not in products_map: + products_map[handle] = Product(handle=handle, title=title) + product_order.append(handle) + elif title and not products_map[handle].title: + products_map[handle].title = title + + products_map[handle].rows.append(vr) + total_variants += 1 + + products = [products_map[h] for h in product_order] + + metadata = { + "source_file": file_path.name, + "products_scanned": len(products), + "variants_scanned": total_variants, + "columns_present": columns, + "has_variant_image_column": "Variant Image" in columns, + "has_option3": "Option3 Name" in columns, + "csv_encoding": encoding, + "bom_detected": bom_detected, + "script_version": SCRIPT_VERSION, + "ran_at": datetime.now(timezone.utc).isoformat(), + } + + return products, columns, metadata + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def option_fields(row: VariantRow) -> list[tuple[str, str, str]]: + """Return list of (field_label, name, value) for non-empty option slots.""" + results = [] + for i, (n, v) in enumerate([ + (row.option1_name, row.option1_value), + (row.option2_name, row.option2_value), + (row.option3_name, row.option3_value), + ], start=1): + if n or v: + results.append((f"Option{i}", n, v)) + return results + + +def describe_whitespace(char: str) -> str: + """Human-readable name for a whitespace character.""" + if char in ABNORMAL_WHITESPACE: + return ABNORMAL_WHITESPACE[char] + if char == " ": + return "space" + if char == "\t": + return "tab" + if char == "\n": + return "newline" + if char == "\r": + return "carriage return" + cp = f"U+{ord(char):04X}" + if unicodedata.category(char).startswith("Z"): + return f"unicode whitespace ({cp})" + return f"invisible char ({cp})" + + +def is_default_title_product(product: Product) -> bool: + """Detect Shopify default single-variant products.""" + if len(product.rows) != 1: + return False + r = product.rows[0] + return (r.option1_name.strip().lower() == "title" + and r.option1_value.strip().lower() == "default title") + + +def slugify(title: str) -> str: + """Replicate Shopify's handle generation from a product title.""" + # Decode HTML entities first + s = html.unescape(title) + # Normalize unicode + s = unicodedata.normalize("NFKD", s) + s = "".join(c for c in s if not unicodedata.combining(c)) + s = s.lower() + # Remove apostrophes (Shopify drops them) + s = s.replace("'", "").replace("\u2019", "") + # Replace non-alphanumeric with hyphens + s = re.sub(r"[^a-z0-9]+", "-", s) + # Collapse consecutive hyphens, strip leading/trailing + s = re.sub(r"-{2,}", "-", s) + s = s.strip("-") + return s + + +# --------------------------------------------------------------------------- +# Check 1: Whitespace issues +# --------------------------------------------------------------------------- + +def check_whitespace(products: list[Product]) -> list[dict]: + issues: list[dict] = [] + + for product in products: + for row in product.rows: + for field_label, name, value in option_fields(row): + # Check both the name and value cells + for cell_type, cell_value in [("Name", name), ("Value", value)]: + if not cell_value: + continue + + field_name = f"{field_label} {cell_type}" + trimmed = cell_value.strip() + chars_detail: list[dict] = [] + + # Leading whitespace + leading = "" + for ch in cell_value: + if ch != ch.strip() or ch in ABNORMAL_WHITESPACE: + leading += ch + else: + break + for ch in leading: + chars_detail.append({ + "position": "leading", + "char": describe_whitespace(ch), + "codepoint": f"U+{ord(ch):04X}", + }) + + # Trailing whitespace + trailing = "" + for ch in reversed(cell_value): + if ch != ch.strip() or ch in ABNORMAL_WHITESPACE: + trailing = ch + trailing + else: + break + for ch in trailing: + chars_detail.append({ + "position": "trailing", + "char": describe_whitespace(ch), + "codepoint": f"U+{ord(ch):04X}", + }) + + # Interior abnormal whitespace (non-breaking spaces, etc.) + interior = cell_value[len(leading):len(cell_value) - len(trailing)] if trailing else cell_value[len(leading):] + for pos, ch in enumerate(interior): + if ch in ABNORMAL_WHITESPACE: + chars_detail.append({ + "position": "interior", + "char": describe_whitespace(ch), + "codepoint": f"U+{ord(ch):04X}", + }) + + if not chars_detail: + continue + + # Determine whitespace type + has_leading = bool(leading) + has_trailing = bool(trailing) + has_interior = any(d["position"] == "interior" for d in chars_detail) + + if has_interior and not has_leading and not has_trailing: + ws_type = "interior_abnormal" + elif has_leading and has_trailing: + ws_type = "both" + elif has_leading: + ws_type = "leading" + elif has_trailing: + ws_type = "trailing" + else: + ws_type = "interior_abnormal" + + issues.append({ + "row": row.row_number, + "handle": row.handle, + "field": field_name, + "original_value": cell_value, + "trimmed_value": trimmed, + "whitespace_type": ws_type, + "characters": chars_detail, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 2: Case inconsistencies +# --------------------------------------------------------------------------- + +def check_case_inconsistencies(products: list[Product]) -> list[dict]: + # Group values by (normalized option name, lowered+stripped value) + # value_groups[key] = { original_value: { "count": N, "handles": set, "rows": list } } + value_groups: dict[tuple[str, str], dict[str, dict]] = defaultdict(lambda: defaultdict(lambda: {"count": 0, "handles": set(), "rows": []})) + + for product in products: + if is_default_title_product(product): + continue + for row in product.rows: + for field_label, name, value in option_fields(row): + if not value: + continue + stripped = value.strip() + if not stripped: + continue + key = (name.strip().lower(), stripped.lower()) + entry = value_groups[key][stripped] + entry["count"] += 1 + entry["handles"].add(row.handle) + entry["rows"].append(row.row_number) + + issues: list[dict] = [] + for (opt_name_lower, val_lower), variants_map in value_groups.items(): + if len(variants_map) < 2: + continue # No inconsistency + + # Find the option name as it appears most often + opt_name_display = opt_name_lower # fallback + for product in products: + for row in product.rows: + for _, name, _ in option_fields(row): + if name.strip().lower() == opt_name_lower: + opt_name_display = name.strip() + + # Build variants list sorted by count descending + variants_list = [] + for val, info in sorted(variants_map.items(), key=lambda x: -x[1]["count"]): + variants_list.append({ + "value": val, + "count": info["count"], + "handles": sorted(info["handles"]), + "rows": sorted(info["rows"]), + }) + + # Suggestion: most frequent wins, title case breaks ties + top_count = variants_list[0]["count"] + tied = [v for v in variants_list if v["count"] == top_count] + if len(tied) == 1: + suggested = tied[0]["value"] + reason = f"Most frequent ({tied[0]['count']} occurrences)" + else: + # Title case preference among tied values + title_cased = [v for v in tied if v["value"] == v["value"].title()] + if title_cased: + suggested = title_cased[0]["value"] + reason = "Tied frequency, title case preferred" + else: + suggested = tied[0]["value"] + reason = "Tied frequency, first encountered chosen" + + issues.append({ + "option_name": opt_name_display, + "normalized_key": val_lower, + "variants_found": variants_list, + "suggested_canonical": suggested, + "suggestion_reason": reason, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 3: Duplicate variants +# --------------------------------------------------------------------------- + +def check_duplicate_variants(products: list[Product]) -> list[dict]: + issues: list[dict] = [] + + for product in products: + if is_default_title_product(product): + continue + + combo_groups: dict[tuple, list[VariantRow]] = defaultdict(list) + for row in product.rows: + key = ( + row.option1_value.strip().lower(), + row.option2_value.strip().lower(), + row.option3_value.strip().lower(), + ) + combo_groups[key].append(row) + + for combo_key, rows in combo_groups.items(): + if len(rows) < 2: + continue + + # Build human-readable option combo string + parts = [] + for val in combo_key: + if val: + parts.append(val) + option_combo = " / ".join(parts) if parts else "(empty)" + + skus = [r.variant_sku for r in rows] + prices = [r.variant_price for r in rows] + inventories = [] + for r in rows: + try: + inventories.append(int(r.variant_inventory_qty)) + except (ValueError, TypeError): + inventories.append(None) + + unique_prices = set(p for p in prices if p) + inv_values = [i for i in inventories if i is not None] + unique_inv = set(inv_values) if inv_values else set() + + issues.append({ + "handle": product.handle, + "option_combo": option_combo, + "rows": [r.row_number for r in rows], + "skus": skus, + "prices_match": len(unique_prices) <= 1, + "prices": list(unique_prices), + "inventory_match": len(unique_inv) <= 1, + "inventories": inv_values, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 4: Missing variant images +# --------------------------------------------------------------------------- + +def check_missing_variant_images(products: list[Product], columns: list[str]) -> list[dict]: + if "Variant Image" not in columns: + return [] + + issues: list[dict] = [] + + for product in products: + if len(product.rows) < 2: + continue # Single-variant products: nothing to compare + + images = [(r, r.variant_image.strip()) for r in product.rows] + with_image = sum(1 for _, img in images if img) + without_image = sum(1 for _, img in images if not img) + + # Only flag if some variants have images and some do not + if with_image == 0 or without_image == 0: + continue + + for row, img in images: + if img: + continue + # Build option values string + parts = [] + if row.option1_value.strip(): + parts.append(row.option1_value.strip()) + if row.option2_value.strip(): + parts.append(row.option2_value.strip()) + if row.option3_value.strip(): + parts.append(row.option3_value.strip()) + + issues.append({ + "handle": product.handle, + "row": row.row_number, + "sku": row.variant_sku, + "option_values": " / ".join(parts), + "siblings_with_images": with_image, + "siblings_without_images": without_image, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 5: Size sequence ordering +# --------------------------------------------------------------------------- + +def load_size_config(assets_dir: Path | None) -> dict: + """Load size alias config or return built-in defaults.""" + defaults = { + "apparel_letter_order": ["XXS", "XS", "S", "M", "L", "XL", "XXL", "2XL", "3XL", "4XL", "5XL"], + "aliases": { + "extra small": "XS", "x-small": "XS", "small": "S", "sm": "S", + "medium": "M", "med": "M", "large": "L", "lg": "L", + "extra large": "XL", "x-large": "XL", "extra-large": "XL", + "xx-large": "XXL", "2x-large": "2XL", "2xl": "2XL", + "3x-large": "3XL", "3xl": "3XL", "one size": "OS", "o/s": "OS", + "os": "OS", "onesize": "OS", + }, + "size_option_names": ["size", "sizes", "dimension", "dimensions"], + } + if assets_dir: + path = assets_dir / "size_aliases.json" + if path.exists(): + try: + with open(path) as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"Warning: Could not load {path}: {e}", file=sys.stderr) + return defaults + + +def detect_size_option(product: Product, size_option_names: list[str]) -> str | None: + """Return which option slot (Option1, Option2, Option3) holds sizes, or None.""" + names_lower = set(n.lower() for n in size_option_names) + for row in product.rows: + if row.option1_name.strip().lower() in names_lower: + return "Option1" + if row.option2_name.strip().lower() in names_lower: + return "Option2" + if row.option3_name.strip().lower() in names_lower: + return "Option3" + return None + + +def get_size_value(row: VariantRow, option_slot: str) -> str: + if option_slot == "Option1": + return row.option1_value.strip() + if option_slot == "Option2": + return row.option2_value.strip() + return row.option3_value.strip() + + +def resolve_size_canonical(value: str, aliases: dict) -> str: + """Map a size value to its canonical form using the alias table.""" + lower = value.strip().lower() + return aliases.get(lower, value.strip()) + + +def detect_size_system(values: list[str], apparel_order: list[str]) -> str: + """Determine if values are apparel letters, numeric, compound, or unknown.""" + apparel_set = set(s.upper() for s in apparel_order) + canonical_values = [v.upper() for v in values] + + if all(v in apparel_set or v == "OS" for v in canonical_values): + return "apparel_letter" + + # Check numeric (including decimals like shoe sizes) + numeric_pattern = re.compile(r"^\d+(\.\d+)?$") + if all(numeric_pattern.match(v) for v in values): + return "numeric" + + # Check compound (waist x inseam) + compound_pattern = re.compile(r"^\d+\s*[x/]\s*\d+$", re.IGNORECASE) + if all(compound_pattern.match(v) for v in values): + return "compound" + + return "unknown" + + +def sort_key_for_size(value: str, aliases: dict, apparel_order: list[str]) -> tuple: + """Return a sort key for a size value.""" + canonical = resolve_size_canonical(value, aliases) + upper = canonical.upper() + + # Check apparel ladder + if upper in apparel_order: + return (0, apparel_order.index(upper), 0) + + # Check numeric + try: + num = float(canonical) + return (1, num, 0) + except ValueError: + pass + + # Check compound (waist x inseam) + match = re.match(r"^(\d+)\s*[x/]\s*(\d+)$", canonical, re.IGNORECASE) + if match: + return (1, float(match.group(1)), float(match.group(2))) + + # Unknown: sort alphabetically at the end + return (2, 0, 0) + + +def check_size_ordering(products: list[Product], size_config: dict) -> list[dict]: + issues: list[dict] = [] + apparel_order = size_config.get("apparel_letter_order", []) + aliases = size_config.get("aliases", {}) + size_names = size_config.get("size_option_names", []) + + for product in products: + if is_default_title_product(product): + continue + + option_slot = detect_size_option(product, size_names) + if not option_slot: + continue + + # Get unique size values in current row order + seen = [] + for row in product.rows: + val = get_size_value(row, option_slot) + canonical = resolve_size_canonical(val, aliases) + if canonical and canonical not in seen: + seen.append(canonical) + + if len(seen) <= 1: + continue # Single size or OS: nothing to order + + size_system = detect_size_system(seen, apparel_order) + + expected = sorted(seen, key=lambda v: sort_key_for_size(v, aliases, apparel_order)) + + if seen != expected: + issues.append({ + "handle": product.handle, + "size_option_field": f"{option_slot} Value", + "current_order": seen, + "expected_order": expected, + "size_system": size_system, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 6: Option value aliases +# --------------------------------------------------------------------------- + +def load_alias_config(assets_dir: Path | None) -> dict: + """Load option name aliases config or return defaults.""" + defaults = { + "option_name_synonyms": [ + ["Color", "Colour"], ["Size", "Sizes", "Dimension", "Dimensions"], + ["Material", "Fabric", "Composition"], ["Style", "Design"], + ["Pattern", "Print"], ["Flavor", "Flavour"], + ], + "color_aliases": { + "grey": ["gray"], "charcoal heather": ["charcoal"], + "navy blue": ["navy"], "heather grey": ["heather gray"], + }, + } + if assets_dir: + path = assets_dir / "option_name_aliases.json" + if path.exists(): + try: + with open(path) as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"Warning: Could not load {path}: {e}", file=sys.stderr) + return defaults + + +def check_option_value_aliases(products: list[Product], alias_config: dict, size_config: dict) -> list[dict]: + issues: list[dict] = [] + + # Build a bidirectional alias lookup from the color_aliases config + known_pairs: dict[str, str] = {} + color_aliases = alias_config.get("color_aliases", {}) + for canonical, variants in color_aliases.items(): + for v in variants: + known_pairs[v.lower()] = canonical.lower() + known_pairs[canonical.lower()] = v.lower() + + # Build size canonical lookup for detecting size aliases like "Extra Large" / "XL" + size_aliases = size_config.get("aliases", {}) + size_option_names = set(n.lower() for n in size_config.get("size_option_names", [])) + + # Collect all values per option name (lowered) + # values_by_option[option_name_lower] = { stripped_value: { "handles": set, "rows": list, "original": str } } + values_by_option: dict[str, dict[str, dict]] = defaultdict(lambda: defaultdict(lambda: {"handles": set(), "rows": [], "original": ""})) + + for product in products: + if is_default_title_product(product): + continue + for row in product.rows: + for _, name, value in option_fields(row): + if not name.strip() or not value.strip(): + continue + opt_lower = name.strip().lower() + val_stripped = value.strip() + entry = values_by_option[opt_lower][val_stripped.lower()] + entry["handles"].add(row.handle) + entry["rows"].append(row.row_number) + if not entry["original"]: + entry["original"] = val_stripped + + # For each option name, check all value pairs + for opt_name, values_map in values_by_option.items(): + value_keys = list(values_map.keys()) + + checked_pairs: set[tuple[str, str]] = set() + + for i, val_a in enumerate(value_keys): + for val_b in value_keys[i + 1:]: + pair = tuple(sorted([val_a, val_b])) + if pair in checked_pairs: + continue + checked_pairs.add(pair) + + detection_method = None + confidence = None + suggested = None + + # Strategy 1: Known color alias map + if val_a in known_pairs and known_pairs[val_a] == val_b: + detection_method = "known_alias_map" + confidence = "high" + elif val_b in known_pairs and known_pairs[val_b] == val_a: + detection_method = "known_alias_map" + confidence = "high" + + # Strategy 2: Size alias map (two values resolve to the same canonical size) + if not detection_method and opt_name in size_option_names: + canon_a = size_aliases.get(val_a, val_a).upper() + canon_b = size_aliases.get(val_b, val_b).upper() + if canon_a == canon_b and val_a != val_b: + detection_method = "known_alias_map" + confidence = "high" + # Suggest the shorter/standard form + suggested = canon_a + + # Strategy 3: Substring containment (shorter is contained in longer) + if not detection_method: + if len(val_a) > 3 and len(val_b) > 3: + if val_a in val_b or val_b in val_a: + detection_method = "substring_match" + confidence = "medium" + + if not detection_method: + continue + + info_a = values_map[val_a] + info_b = values_map[val_b] + + # Determine if they appear on the same product + same_product = bool(info_a["handles"] & info_b["handles"]) + + # Suggest the more frequent value as canonical + if len(info_a["rows"]) >= len(info_b["rows"]): + suggested = info_a["original"] + else: + suggested = info_b["original"] + + # Find a display name for the option + opt_display = opt_name + for product in products: + for row in product.rows: + for _, name, _ in option_fields(row): + if name.strip().lower() == opt_name: + opt_display = name.strip() + break + + issues.append({ + "option_name": opt_display, + "values": [info_a["original"], info_b["original"]], + "detection_method": detection_method, + "confidence": confidence, + "suggested_canonical": suggested, + "same_product": same_product, + "handles": sorted(info_a["handles"] | info_b["handles"]), + "rows_per_value": { + info_a["original"]: sorted(info_a["rows"]), + info_b["original"]: sorted(info_b["rows"]), + }, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 7: Option name inconsistencies +# --------------------------------------------------------------------------- + +def check_option_name_inconsistencies(products: list[Product], alias_config: dict) -> list[dict]: + # Build synonym lookup: lowered name -> group index + synonym_groups = alias_config.get("option_name_synonyms", []) + name_to_group: dict[str, int] = {} + for idx, group in enumerate(synonym_groups): + for name in group: + name_to_group[name.lower()] = idx + + # Collect all option names and which products use them + # name_usage[original_name] = set of handles + name_usage: dict[str, set[str]] = defaultdict(set) + + for product in products: + if is_default_title_product(product): + continue + for row in product.rows: + for _, name, _ in option_fields(row): + stripped = name.strip() + if stripped: + name_usage[stripped].add(product.handle) + + # Group names by synonym group or by lowered form + groups_found: dict[Any, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set)) + + for name, handles in name_usage.items(): + lower = name.lower() + group_key = name_to_group.get(lower, f"_solo_{lower}") + groups_found[group_key][name] |= handles + + issues: list[dict] = [] + for group_key, names_map in groups_found.items(): + if len(names_map) < 2: + continue + + # Find suggested canonical: most products win, then US English spelling + names_sorted = sorted(names_map.items(), key=lambda x: -len(x[1])) + top_count = len(names_sorted[0][1]) + tied = [n for n, h in names_sorted if len(h) == top_count] + + suggested = tied[0] + reason = f"Most frequent ({top_count} products)" + + if len(tied) > 1: + # Prefer US English: Color over Colour, Flavor over Flavour + us_spellings = {"color", "flavor", "size", "material", "style", "pattern", "scent", "length", "width", "weight", "volume"} + for t in tied: + if t.lower() in us_spellings: + suggested = t + reason = "Tied frequency, US English preferred" + break + + products_per_name = {name: sorted(handles) for name, handles in names_map.items()} + + issues.append({ + "names_found": list(names_map.keys()), + "products_per_name": products_per_name, + "suggested_canonical": suggested, + "suggestion_reason": reason, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Check 8: Handle/Title drift +# --------------------------------------------------------------------------- + +def check_handle_title_drift(products: list[Product]) -> list[dict]: + issues: list[dict] = [] + + for product in products: + title = product.title.strip() + if not title: + continue + + expected = slugify(title) + actual = product.handle.strip() + + if expected == actual: + continue + + # Classify the difference + # Check if it is just a gendered suffix difference + gendered_suffixes = ["-mens", "-womens", "-men-s", "-women-s", "-unisex", "-kids", "-youth", "-boys", "-girls"] + stripped_expected = expected + stripped_actual = actual + for suffix in gendered_suffixes: + stripped_expected = stripped_expected.removesuffix(suffix) + stripped_actual = stripped_actual.removesuffix(suffix) + + if stripped_expected == stripped_actual: + diff_type = "gendered_suffix_variation" + elif expected.startswith(actual) or actual.startswith(expected): + diff_type = "truncated" + elif abs(len(expected) - len(actual)) <= 3: + diff_type = "minor_punctuation" + else: + diff_type = "significant_mismatch" + + issues.append({ + "handle": actual, + "title": title, + "expected_handle": expected, + "difference_type": diff_type, + }) + + return issues + + +# --------------------------------------------------------------------------- +# Additional checks: warnings +# --------------------------------------------------------------------------- + +def check_warnings(products: list[Product], columns: list[str], metadata: dict) -> list[dict]: + """Generate non-issue warnings for structural or edge-case observations.""" + warnings: list[dict] = [] + + # BOM detected + if metadata.get("bom_detected"): + warnings.append({ + "code": "bom_detected", + "message": "UTF-8 BOM detected in CSV file. This can cause issues with column name matching.", + "details": {}, + }) + + # Empty option values (name populated, value empty) + for product in products: + if is_default_title_product(product): + continue + for row in product.rows: + for field_label, name, value in option_fields(row): + if name.strip() and not value.strip(): + warnings.append({ + "code": "empty_option_value", + "message": f"Option name '{name.strip()}' is set but value is empty on row {row.row_number}.", + "details": { + "handle": row.handle, + "row": row.row_number, + "field": f"{field_label} Value", + "option_name": name.strip(), + }, + }) + + # Option column position inconsistency + option_positions: dict[str, set[str]] = defaultdict(set) # name_lower -> set of positions + for product in products: + if is_default_title_product(product): + continue + for row in product.rows: + for field_label, name, _ in option_fields(row): + if name.strip(): + option_positions[name.strip().lower()].add(field_label) + + for name_lower, positions in option_positions.items(): + if len(positions) > 1: + warnings.append({ + "code": "option_position_inconsistency", + "message": f"Option '{name_lower}' appears in multiple column positions: {sorted(positions)}.", + "details": {"option_name": name_lower, "positions": sorted(positions)}, + }) + + # HTML entities in option values + entity_pattern = re.compile(r"&\w+;|&#\d+;|&#x[\da-fA-F]+;") + for product in products: + for row in product.rows: + for field_label, _, value in option_fields(row): + if value and entity_pattern.search(value): + warnings.append({ + "code": "html_entity_in_option", + "message": f"HTML entity found in {field_label} Value on row {row.row_number}: '{value.strip()}'.", + "details": { + "handle": row.handle, + "row": row.row_number, + "field": f"{field_label} Value", + "value": value.strip(), + }, + }) + + # Option values exceeding 255 characters + for product in products: + for row in product.rows: + for field_label, _, value in option_fields(row): + if value and len(value.strip()) > SHOPIFY_OPTION_VALUE_MAX_LENGTH: + warnings.append({ + "code": "option_value_too_long", + "message": f"{field_label} Value on row {row.row_number} exceeds {SHOPIFY_OPTION_VALUE_MAX_LENGTH} characters ({len(value.strip())} chars).", + "details": { + "handle": row.handle, + "row": row.row_number, + "field": f"{field_label} Value", + "length": len(value.strip()), + }, + }) + + # Default Title products detected + for product in products: + if is_default_title_product(product): + warnings.append({ + "code": "default_title_product", + "message": f"Product '{product.handle}' uses Shopify default single-variant pattern (Title / Default Title). Skipped from option checks.", + "details": {"handle": product.handle}, + }) + + return warnings + + +# --------------------------------------------------------------------------- +# Main runner +# --------------------------------------------------------------------------- + +def run_audit(csv_path: Path, assets_dir: Path | None) -> dict: + """Run all checks and return the full audit JSON.""" + products, columns, metadata = parse_csv(csv_path) + + size_config = load_size_config(assets_dir) + alias_config = load_alias_config(assets_dir) + + # Filter out default-title products for counting + active_products = [p for p in products if not is_default_title_product(p)] + + whitespace = check_whitespace(products) + case_issues = check_case_inconsistencies(products) + duplicates = check_duplicate_variants(products) + missing_images = check_missing_variant_images(products, columns) + size_order = check_size_ordering(products, size_config) + value_aliases = check_option_value_aliases(products, alias_config) + name_issues = check_option_name_inconsistencies(products, alias_config) + handle_drift = check_handle_title_drift(products) + warnings = check_warnings(products, columns, metadata) + + total_issues = ( + len(whitespace) + len(case_issues) + len(duplicates) + + len(missing_images) + len(size_order) + len(value_aliases) + + len(name_issues) + len(handle_drift) + ) + metadata["total_issues_found"] = total_issues + + return { + "metadata": metadata, + "issues": { + "whitespace": whitespace, + "case_inconsistencies": case_issues, + "duplicate_variants": duplicates, + "missing_variant_images": missing_images, + "size_ordering": size_order, + "option_value_aliases": value_aliases, + "option_name_inconsistencies": name_issues, + "handle_title_drift": handle_drift, + }, + "warnings": warnings, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Audit a Shopify product CSV for variant option inconsistencies." + ) + parser.add_argument("csv_path", type=Path, help="Path to Shopify product CSV") + parser.add_argument( + "--assets-dir", type=Path, default=None, + help="Path to assets/ directory with alias config files" + ) + args = parser.parse_args() + + if not args.csv_path.exists(): + print(f"Fatal: File not found: {args.csv_path}", file=sys.stderr) + return 1 + + if not args.csv_path.is_file(): + print(f"Fatal: Not a file: {args.csv_path}", file=sys.stderr) + return 1 + + try: + result = run_audit(args.csv_path, args.assets_dir) + except Exception as e: + print(f"Fatal: Audit failed: {e}", file=sys.stderr) + return 1 + + # Convert sets to sorted lists for JSON serialization + json.dump(result, sys.stdout, indent=2, default=lambda o: sorted(o) if isinstance(o, set) else str(o)) + print() # trailing newline + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6481b12db83cc2d66672fd12c11fcd141352f823 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 22:33:35 +0600 Subject: [PATCH 08/24] feat: Enhance option value alias suggestion logic and update function call in audit process --- .../scripts/normalize_audit.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skills/variant-option-normalizer/scripts/normalize_audit.py b/skills/variant-option-normalizer/scripts/normalize_audit.py index bc22a1c..6463766 100644 --- a/skills/variant-option-normalizer/scripts/normalize_audit.py +++ b/skills/variant-option-normalizer/scripts/normalize_audit.py @@ -765,11 +765,12 @@ def check_option_value_aliases(products: list[Product], alias_config: dict, size # Determine if they appear on the same product same_product = bool(info_a["handles"] & info_b["handles"]) - # Suggest the more frequent value as canonical - if len(info_a["rows"]) >= len(info_b["rows"]): - suggested = info_a["original"] - else: - suggested = info_b["original"] + # Suggest the more frequent value as canonical (unless already set by size alias) + if not suggested: + if len(info_a["rows"]) >= len(info_b["rows"]): + suggested = info_a["original"] + else: + suggested = info_b["original"] # Find a display name for the option opt_display = opt_name @@ -1026,7 +1027,7 @@ def run_audit(csv_path: Path, assets_dir: Path | None) -> dict: duplicates = check_duplicate_variants(products) missing_images = check_missing_variant_images(products, columns) size_order = check_size_ordering(products, size_config) - value_aliases = check_option_value_aliases(products, alias_config) + value_aliases = check_option_value_aliases(products, alias_config, size_config) name_issues = check_option_name_inconsistencies(products, alias_config) handle_drift = check_handle_title_drift(products) warnings = check_warnings(products, columns, metadata) From 39699d0a7e1cdfe2a871154064070c27ea9968cc Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 22:36:35 +0600 Subject: [PATCH 09/24] feat: Update compatibility information and enhance JSON output schema documentation for audit script --- skills/variant-option-normalizer/SKILL.md | 51 +++++- .../references/json-schema.md | 165 ++++++++++++++++++ .../variant-option-normalizer/skillshelf.yaml | 2 +- 3 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 skills/variant-option-normalizer/references/json-schema.md diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 3225113..af2c247 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -4,6 +4,7 @@ description: >- Detects inconsistent Shopify variant option values, proposes a canonical naming system, and produces a corrected CSV with a change log. license: Apache-2.0 +compatibility: "Requires Python 3.10+. Falls back to pure LLM analysis if Python is unavailable." --- # Normalize and Repair Variant Options @@ -16,6 +17,45 @@ For reference on the expected output format, see [references/example-output.md]( --- +## Script Execution + +This skill uses a hybrid approach: a Python script handles deterministic checks (whitespace, case, duplicates, size ordering, missing images) and outputs structured JSON. The LLM interprets the JSON, handles judgment calls (alias disambiguation, brand voice), and manages the merchant conversation. + +**Before analyzing any CSV yourself, run the audit script:** + +``` +python3 scripts/normalize_audit.py --assets-dir assets/ +``` + +The script outputs JSON to stdout. Capture the full output and use it as the basis for the audit report. + +**Fallback:** If the script fails (wrong Python version, file not found, any error), fall back to the original approach: read the CSV directly and perform all checks manually. Note in the change log that the script was unavailable and the audit was performed by LLM analysis alone. + +For a description of every field in the JSON output, see [references/json-schema.md](references/json-schema.md). + +--- + +## Interpreting Script Output + +When presenting the script's JSON findings to the merchant: + +1. **Do not show raw JSON.** Translate every finding into the Markdown audit report format described in the Conversation Flow. +2. **Map confidence levels to presentation style:** + - `high` confidence: state as a finding ("These values are aliases and should be merged.") + - `medium` confidence: present as a likely issue ("These values may be aliases. Can you confirm?") + - `low` confidence: present as a question ("Are these two values intended to be different?") +3. **Handle warnings selectively.** Surface BOM detection, encoding issues, empty option values, and HTML entities in option values. Skip routine metadata like script version and timestamp. +4. **For handle/title drift:** Apply your own judgment about acceptable differences. The script flags all mismatches; filter out gendered suffix variations (e.g., dropping "Men's" or "Women's" from handles) before presenting to the merchant. +5. **For alias candidates with `same_product: true`:** These are almost certainly errors. Present them confidently. Cross-product aliases (`same_product: false`) may be intentional and should be framed as questions. + +**Supplement the script's findings with LLM-only checks:** + +- **Semantic alias detection:** Values that are semantically equivalent but not in the known alias map (e.g., "Crimson" and "Red", "Burgundy" and "Wine"). Use your knowledge of colors, materials, and sizing to identify candidates. +- **Context-aware judgment:** Determine whether ambiguous values are sizes, colors, or something else based on the option name and product type. +- **Brand voice alignment:** Consider whether the store's positioning suggests spelled-out sizes ("Extra Large") or abbreviations ("XL"). + +--- + ## Voice and Approach You are a catalog operations specialist helping an ecommerce team clean up variant data. Be direct and precise. The merchant knows their products better than you do. Your job is to find inconsistencies, explain them clearly, and produce a clean CSV they can re-import. Do not narrate your process or over-explain. When transitioning between steps, keep it brief. Match the merchant's level of formality. @@ -32,11 +72,12 @@ Example prompt: "Please share your Shopify product CSV. You can paste the rows d Once you have the CSV: -1. Parse all rows. Identify every unique product handle. -2. For each product, extract all Option1/Option2/Option3 Name and Value cells. -3. Build a master catalog of every unique option name and every unique option value, both per-product and across the full file. -4. Run the Detection Categories analysis (see below). -5. Present findings as a structured audit report, grouped by issue type. +1. Run the audit script: `python3 scripts/normalize_audit.py --assets-dir assets/` +2. Parse the JSON output. Use the metadata section for summary counts. +3. Translate each non-empty issues array into the corresponding audit report section. +4. Apply the Interpreting Script Output rules above to filter, frame, and supplement the findings. +5. Run your own semantic checks for aliases, context-dependent values, and brand voice considerations that the script cannot detect. +6. Present findings as a structured audit report, grouped by issue type. Do not ask clarifying questions before producing the audit. Show the findings first so the merchant can see what needs attention. diff --git a/skills/variant-option-normalizer/references/json-schema.md b/skills/variant-option-normalizer/references/json-schema.md new file mode 100644 index 0000000..1a39ea3 --- /dev/null +++ b/skills/variant-option-normalizer/references/json-schema.md @@ -0,0 +1,165 @@ +# Audit Script JSON Output Schema + +This document describes the JSON output produced by `scripts/normalize_audit.py`. The LLM reads this schema to interpret the script's findings and present them to the merchant. + +--- + +## Top-Level Structure + +```json +{ + "metadata": { ... }, + "issues": { ... }, + "warnings": [ ... ] +} +``` + +--- + +## metadata + +| Field | Type | Description | +|---|---|---| +| source_file | string | Filename of the input CSV | +| products_scanned | integer | Number of unique product handles found | +| variants_scanned | integer | Total number of data rows (excluding header) | +| total_issues_found | integer | Sum of all issues across all check categories | +| columns_present | string[] | Column headers from the CSV, in order | +| has_variant_image_column | boolean | Whether the CSV contains a "Variant Image" column | +| has_option3 | boolean | Whether the CSV contains "Option3 Name" column | +| csv_encoding | string | Detected file encoding (e.g., "utf-8-sig", "latin-1") | +| bom_detected | boolean | Whether a UTF-8 BOM was found at the start of the file | +| script_version | string | Version of the audit script | +| ran_at | string | ISO 8601 timestamp of when the audit ran | + +--- + +## issues + +Each key is a check category. The value is an array of issue objects. Empty arrays mean no issues found for that category. + +### issues.whitespace + +Each entry represents one cell with whitespace problems. + +| Field | Type | Description | +|---|---|---| +| row | integer | 1-based row number in the CSV (header = row 1) | +| handle | string | Product handle | +| field | string | Which field has the issue, e.g., "Option2 Value" | +| original_value | string | The raw value as it appears in the CSV | +| trimmed_value | string | The value after stripping whitespace | +| whitespace_type | string | One of: "leading", "trailing", "both", "interior_abnormal" | +| characters | object[] | Details of each problematic character: position, human-readable name, Unicode codepoint | + +### issues.case_inconsistencies + +Each entry represents a group of values that differ only in casing. + +| Field | Type | Description | +|---|---|---| +| option_name | string | The option name as displayed (e.g., "Color") | +| normalized_key | string | Lowercased, stripped value used for grouping | +| variants_found | object[] | Each variant: value, count, handles[], rows[] | +| suggested_canonical | string | Recommended canonical form | +| suggestion_reason | string | Why this form was chosen (e.g., "Most frequent") | + +### issues.duplicate_variants + +Each entry represents a set of rows with identical option combinations on the same product. + +| Field | Type | Description | +|---|---|---| +| handle | string | Product handle | +| option_combo | string | Human-readable combo, e.g., "s / deep teal" | +| rows | integer[] | Row numbers of the duplicate rows | +| skus | string[] | SKUs on each duplicate row | +| prices_match | boolean | Whether all duplicates have the same price | +| prices | string[] | Distinct price values found | +| inventory_match | boolean | Whether all duplicates have the same inventory | +| inventories | integer[] | Inventory quantities on each row | + +### issues.missing_variant_images + +Each entry represents a variant row missing an image when sibling variants have images. + +| Field | Type | Description | +|---|---|---| +| handle | string | Product handle | +| row | integer | Row number | +| sku | string | Variant SKU | +| option_values | string | Human-readable option combo, e.g., "M / Black" | +| siblings_with_images | integer | How many sibling variants have images | +| siblings_without_images | integer | How many sibling variants lack images | + +### issues.size_ordering + +Each entry represents a product with out-of-order size variants. + +| Field | Type | Description | +|---|---|---| +| handle | string | Product handle | +| size_option_field | string | Which column holds sizes, e.g., "Option1 Value" | +| current_order | string[] | Size values in their current row order | +| expected_order | string[] | Size values in the correct order | +| size_system | string | One of: "apparel_letter", "numeric", "compound", "unknown" | + +### issues.option_value_aliases + +Each entry represents a pair of values that may refer to the same thing. + +| Field | Type | Description | +|---|---|---| +| option_name | string | The option name (e.g., "Color", "Size") | +| values | string[] | The two values that may be aliases | +| detection_method | string | "known_alias_map" or "substring_match" | +| confidence | string | "high", "medium", or "low" | +| suggested_canonical | string | Recommended canonical form | +| same_product | boolean | Whether both values appear on the same product handle | +| handles | string[] | All affected product handles | +| rows_per_value | object | Maps each value to its row numbers | + +### issues.option_name_inconsistencies + +Each entry represents a group of option names that should be unified. + +| Field | Type | Description | +|---|---|---| +| names_found | string[] | The distinct option names found | +| products_per_name | object | Maps each name to its product handles | +| suggested_canonical | string | Recommended canonical name | +| suggestion_reason | string | Why this name was chosen | + +### issues.handle_title_drift + +Each entry represents a product where the handle does not match the slugified title. + +| Field | Type | Description | +|---|---|---| +| handle | string | Actual handle | +| title | string | Product title | +| expected_handle | string | What the handle would be if generated from the title | +| difference_type | string | One of: "gendered_suffix_variation", "truncated", "minor_punctuation", "significant_mismatch" | + +--- + +## warnings + +An array of non-issue observations. Each warning has: + +| Field | Type | Description | +|---|---|---| +| code | string | Machine-readable code (see below) | +| message | string | Human-readable description | +| details | object | Additional context (varies by code) | + +### Warning codes + +| Code | Meaning | +|---|---| +| bom_detected | UTF-8 BOM found in the CSV file | +| empty_option_value | Option name is set but value is empty | +| option_position_inconsistency | Same option name appears in different column positions across products | +| html_entity_in_option | HTML entity found in an option value (likely a paste error) | +| option_value_too_long | Option value exceeds Shopify's 255-character limit | +| default_title_product | Product uses Shopify's default single-variant pattern; skipped from checks | diff --git a/skills/variant-option-normalizer/skillshelf.yaml b/skills/variant-option-normalizer/skillshelf.yaml index e024856..cf3fc35 100644 --- a/skills/variant-option-normalizer/skillshelf.yaml +++ b/skills/variant-option-normalizer/skillshelf.yaml @@ -17,7 +17,7 @@ tags: - bulk-edit date_added: "2026-04-05" -date_updated: "2026-04-05" +date_updated: "2026-04-07" author: name: Raqib Abdullah From dcc001c7307a3683ff9b50e4918321e351e1e66e Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 22:51:38 +0600 Subject: [PATCH 10/24] feat: Enhance example output documentation with additional audit details and JSON metadata --- .../references/example-output.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/skills/variant-option-normalizer/references/example-output.md b/skills/variant-option-normalizer/references/example-output.md index d6a32a3..fce4b64 100644 --- a/skills/variant-option-normalizer/references/example-output.md +++ b/skills/variant-option-normalizer/references/example-output.md @@ -2,10 +2,33 @@ This example demonstrates the full output of the Normalize and Repair Variant Options skill using fictional data from SummitGear Co., an outdoor apparel brand. +The audit is generated by running the deterministic Python script first, then interpreting its JSON output. Structural issues (whitespace, case, duplicates, missing images, size ordering) come from the script. Semantic issues (alias reasoning, brand voice alignment) come from LLM analysis on top of the script results. + --- ## Turn 1: Audit Report +The script ran successfully: + +``` +python3 scripts/normalize_audit.py summitgear-products.csv --assets-dir assets/ +``` + +```json +{ + "metadata": { + "products_scanned": 4, + "variants_scanned": 22, + "total_issues_found": 9, + "has_variant_image_column": true, + "csv_encoding": "utf-8-sig", + "bom_detected": false + } +} +``` + +LLM analysis identified 2 additional semantic alias candidates not in the known alias map (Grey/Gray and XL/Extra Large), bringing the total presented to the merchant to 11. + ### Variant Option Audit **Products scanned:** 4 From 1cef34ed87c61a67d9607255f819c455dd87d5be Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 23:19:41 +0600 Subject: [PATCH 11/24] feat: Expand option name aliases and enhance size alias mappings for better variant normalization --- .../assets/option_name_aliases.json | 39 +++++--- .../assets/size_aliases.json | 92 ++++++++++++++++++- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/skills/variant-option-normalizer/assets/option_name_aliases.json b/skills/variant-option-normalizer/assets/option_name_aliases.json index 6f83097..c35bf20 100644 --- a/skills/variant-option-normalizer/assets/option_name_aliases.json +++ b/skills/variant-option-normalizer/assets/option_name_aliases.json @@ -1,22 +1,39 @@ { "option_name_synonyms": [ - ["Color", "Colour"], - ["Size", "Sizes", "Dimension", "Dimensions"], - ["Material", "Fabric", "Composition"], + ["Color", "Colour", "Colorway", "Colourway", "Shade"], + ["Size", "Sizes", "Sizing", "Dimension", "Dimensions"], + ["Material", "Fabric", "Composition", "Fiber", "Fibre", "Fabric Content", "Fabric Composition"], ["Style", "Design"], - ["Pattern", "Print"], - ["Flavor", "Flavour"], - ["Scent", "Fragrance"], - ["Length", "Inseam"], - ["Width", "Waist"], + ["Fit", "Cut", "Silhouette"], + ["Pattern", "Print", "Graphic"], + ["Flavor", "Flavour", "Taste", "Variety"], + ["Scent", "Fragrance", "Aroma"], + ["Length", "Inseam", "Inseam Length", "Leg Length"], + ["Width", "Waist", "Waist Size"], ["Weight", "Mass"], - ["Volume", "Capacity"] + ["Volume", "Capacity", "Storage"], + ["Finish", "Texture", "Surface"], + ["Pack", "Pack Size", "Bundle", "Bundle Size", "Set", "Count", "Quantity"], + ["Strength", "Potency", "Concentration", "Dose", "Dosage"], + ["Edition", "Version", "Gen", "Generation"] ], "color_aliases": { - "grey": ["gray"], + "gray": ["grey"], + "dark gray": ["dark grey"], + "light gray": ["light grey"], + "heather gray": ["heather grey"], + "charcoal gray": ["charcoal grey"], + "slate gray": ["slate grey"], + "steel gray": ["steel grey"], + "silver gray": ["silver grey"], + "warm gray": ["warm grey"], + "cool gray": ["cool grey"], + "medium gray": ["medium grey"], + "dark heather gray": ["dark heather grey", "dark heather"], + "marl gray": ["marl grey"], "charcoal heather": ["charcoal"], "navy blue": ["navy"], - "heather grey": ["heather gray"], + "off-white": ["off white", "offwhite"], "burgundy": ["wine"], "cream": ["ivory", "off-white", "offwhite"], "tan": ["khaki", "beige"], diff --git a/skills/variant-option-normalizer/assets/size_aliases.json b/skills/variant-option-normalizer/assets/size_aliases.json index 28a8112..fbf033d 100644 --- a/skills/variant-option-normalizer/assets/size_aliases.json +++ b/skills/variant-option-normalizer/assets/size_aliases.json @@ -2,35 +2,123 @@ "apparel_letter_order": [ "XXS", "XS", "S", "M", "L", "XL", "XXL", "2XL", "3XL", "4XL", "5XL" ], + "plus_order": [ + "0X", "1X", "2X", "3X", "4X", "5X" + ], + "toddler_order": [ + "2T", "3T", "4T", "5T", "6T" + ], + "youth_order": [ + "YXS", "YS", "YM", "YL", "YXL" + ], "aliases": { "extra extra small": "XXS", + "double extra small": "XXS", + "xxs": "XXS", + "extra small": "XS", "x-small": "XS", + "x small": "XS", "xs": "XS", + "small": "S", "sm": "S", + "medium": "M", "med": "M", + "large": "L", "lg": "L", + "extra large": "XL", "x-large": "XL", "extra-large": "XL", + "x large": "XL", + "xl": "XL", + + "extra extra large": "XXL", + "double extra large": "XXL", "xx-large": "XXL", + "xx large": "XXL", + "xxl": "XXL", + "2x-large": "2XL", + "2 xl": "2XL", + "2x large": "2XL", + "2xlarge": "2XL", "2xl": "2XL", + "3x-large": "3XL", + "3 xl": "3XL", + "3x large": "3XL", + "3xlarge": "3XL", "3xl": "3XL", + "triple extra large": "3XL", + "xxxl": "3XL", + "4x-large": "4XL", + "4 xl": "4XL", + "4x large": "4XL", + "4xlarge": "4XL", "4xl": "4XL", + "xxxxl": "4XL", + "5x-large": "5XL", + "5 xl": "5XL", + "5x large": "5XL", + "5xlarge": "5XL", "5xl": "5XL", + "xxxxxl": "5XL", + + "0x": "0X", + "1x": "1X", + "2x": "2X", + "3x": "3X", + "4x": "4X", + "5x": "5X", + + "2t": "2T", + "3t": "3T", + "4t": "4T", + "5t": "5T", + "6t": "6T", + + "yxs": "YXS", + "youth xs": "YXS", + "youth extra small": "YXS", + + "ys": "YS", + "youth s": "YS", + "youth small": "YS", + + "ym": "YM", + "youth m": "YM", + "youth medium": "YM", + + "yl": "YL", + "youth l": "YL", + "youth large": "YL", + + "yxl": "YXL", + "youth xl": "YXL", + "youth extra large": "YXL", + "one size": "OS", + "one-size": "OS", "o/s": "OS", "os": "OS", - "onesize": "OS" + "onesize": "OS", + "one size fits all": "OS", + "one size fits most": "OS", + "osfa": "OS", + "osfm": "OS", + "free size": "OS" }, "size_option_names": [ - "size", "sizes", "dimension", "dimensions" + "size", "sizes", "sizing", + "dimension", "dimensions", + "waist", "inseam", "length", "width", + "chest", "neck", + "shoe size", "hat size", "ring size" ] } From ef242dca319232cece83d2fe4f7e0f0d5e35b1df Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 23:25:34 +0600 Subject: [PATCH 12/24] feat: Enhance option name aliases with additional synonyms for better variant normalization --- .../assets/option_name_aliases.json | 51 ++++++-- .../assets/size_aliases.json | 113 ++++++++++++++++-- 2 files changed, 146 insertions(+), 18 deletions(-) diff --git a/skills/variant-option-normalizer/assets/option_name_aliases.json b/skills/variant-option-normalizer/assets/option_name_aliases.json index c35bf20..3cd6c05 100644 --- a/skills/variant-option-normalizer/assets/option_name_aliases.json +++ b/skills/variant-option-normalizer/assets/option_name_aliases.json @@ -1,21 +1,48 @@ { "option_name_synonyms": [ ["Color", "Colour", "Colorway", "Colourway", "Shade"], + ["Size", "Sizes", "Sizing", "Dimension", "Dimensions"], + ["Material", "Fabric", "Composition", "Fiber", "Fibre", "Fabric Content", "Fabric Composition"], + ["Style", "Design"], + ["Fit", "Cut", "Silhouette"], + ["Pattern", "Print", "Graphic"], + ["Flavor", "Flavour", "Taste", "Variety"], + ["Scent", "Fragrance", "Aroma"], - ["Length", "Inseam", "Inseam Length", "Leg Length"], - ["Width", "Waist", "Waist Size"], - ["Weight", "Mass"], - ["Volume", "Capacity", "Storage"], + + ["Length", "Leg Length"], + + ["Inseam", "Inseam Length"], + + ["Waist", "Waist Size", "Waist Measurement"], + + ["Weight", "Mass", "Net Weight", "Net Wt"], + + ["Volume", "Capacity"], + ["Finish", "Texture", "Surface"], - ["Pack", "Pack Size", "Bundle", "Bundle Size", "Set", "Count", "Quantity"], + + ["Pack", "Pack Size", "Bundle", "Bundle Size", "Set", "Count", "Quantity", "Servings", "Serving Count", "Serving Size"], + ["Strength", "Potency", "Concentration", "Dose", "Dosage"], - ["Edition", "Version", "Gen", "Generation"] + + ["Edition", "Version", "Gen", "Generation"], + + ["Sleeve", "Sleeve Length"], + + ["Age", "Age Group", "Age Range", "Ages"], + + ["Voltage", "Wattage", "Power", "Watts"], + + ["Metal", "Metal Type", "Metal Color", "Metal Finish"], + + ["Resistance", "Resistance Level", "Tension"] ], "color_aliases": { "gray": ["grey"], @@ -33,11 +60,13 @@ "marl gray": ["marl grey"], "charcoal heather": ["charcoal"], "navy blue": ["navy"], - "off-white": ["off white", "offwhite"], - "burgundy": ["wine"], - "cream": ["ivory", "off-white", "offwhite"], + "cream": ["ivory"], + "off-white": ["off white", "offwhite", "off white"], "tan": ["khaki", "beige"], - "fuchsia": ["hot pink"], - "magenta": ["hot pink"] + "hot pink": ["fuchsia", "magenta"], + "wine": ["maroon", "burgundy"], + "plum": ["eggplant"], + "lavender": ["lilac"], + "blush": ["rose"] } } diff --git a/skills/variant-option-normalizer/assets/size_aliases.json b/skills/variant-option-normalizer/assets/size_aliases.json index fbf033d..955bd35 100644 --- a/skills/variant-option-normalizer/assets/size_aliases.json +++ b/skills/variant-option-normalizer/assets/size_aliases.json @@ -11,6 +11,15 @@ "youth_order": [ "YXS", "YS", "YM", "YL", "YXL" ], + "infant_order": [ + "NB", "0-3M", "3-6M", "6-9M", "6-12M", "12-18M", "18-24M" + ], + "petite_order": [ + "PS", "PM", "PL", "PXL" + ], + "tall_order": [ + "TS", "TM", "TL", "TXL" + ], "aliases": { "extra extra small": "XXS", "double extra small": "XXS", @@ -86,23 +95,108 @@ "yxs": "YXS", "youth xs": "YXS", "youth extra small": "YXS", - "ys": "YS", "youth s": "YS", "youth small": "YS", - "ym": "YM", "youth m": "YM", "youth medium": "YM", - "yl": "YL", "youth l": "YL", "youth large": "YL", - "yxl": "YXL", "youth xl": "YXL", "youth extra large": "YXL", + "newborn": "NB", + "nb": "NB", + "new born": "NB", + + "0-3 months": "0-3M", + "0/3m": "0-3M", + "0-3mo": "0-3M", + "0-3 mo": "0-3M", + "0 to 3 months": "0-3M", + "preemie": "NB", + + "3-6 months": "3-6M", + "3/6m": "3-6M", + "3-6mo": "3-6M", + "3-6 mo": "3-6M", + "3 to 6 months": "3-6M", + + "6-9 months": "6-9M", + "6/9m": "6-9M", + "6-9mo": "6-9M", + "6-9 mo": "6-9M", + "6 to 9 months": "6-9M", + + "6-12 months": "6-12M", + "6/12m": "6-12M", + "6-12mo": "6-12M", + "6-12 mo": "6-12M", + "6 to 12 months": "6-12M", + + "12-18 months": "12-18M", + "12/18m": "12-18M", + "12-18mo": "12-18M", + "12-18 mo": "12-18M", + "12 to 18 months": "12-18M", + + "18-24 months": "18-24M", + "18/24m": "18-24M", + "18-24mo": "18-24M", + "18-24 mo": "18-24M", + "18 to 24 months": "18-24M", + + "petite s": "PS", + "petite small": "PS", + "p/s": "PS", + "p-s": "PS", + "ps": "PS", + + "petite m": "PM", + "petite medium": "PM", + "p/m": "PM", + "p-m": "PM", + "pm": "PM", + + "petite l": "PL", + "petite large": "PL", + "p/l": "PL", + "p-l": "PL", + "pl": "PL", + + "petite xl": "PXL", + "petite extra large": "PXL", + "p/xl": "PXL", + "p-xl": "PXL", + "pxl": "PXL", + + "tall s": "TS", + "tall small": "TS", + "t/s": "TS", + "t-s": "TS", + "ts": "TS", + + "tall m": "TM", + "tall medium": "TM", + "t/m": "TM", + "t-m": "TM", + "tm": "TM", + + "tall l": "TL", + "tall large": "TL", + "t/l": "TL", + "t-l": "TL", + "tl": "TL", + + "tall xl": "TXL", + "tall extra large": "TXL", + "t/xl": "TXL", + "t-xl": "TXL", + "txl": "TXL", + "one size": "OS", "one-size": "OS", "o/s": "OS", @@ -112,13 +206,18 @@ "one size fits most": "OS", "osfa": "OS", "osfm": "OS", - "free size": "OS" + "free size": "OS", + "adjustable": "OS", + "universal": "OS" }, "size_option_names": [ "size", "sizes", "sizing", "dimension", "dimensions", "waist", "inseam", "length", "width", - "chest", "neck", - "shoe size", "hat size", "ring size" + "chest", "neck", "sleeve", "sleeve length", + "shoe size", "hat size", "ring size", + "cup size", "band size", + "age", "age group", + "pack size", "net weight" ] } From 80b1b5c2ac1166054f34f9f07f827f890af3d56c Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Tue, 7 Apr 2026 23:41:08 +0600 Subject: [PATCH 13/24] feat: Update size detection logic and expand size system classifications for improved variant normalization --- skills/variant-option-normalizer/SKILL.md | 12 ++- .../references/example-output.md | 4 +- .../references/json-schema.md | 2 +- .../scripts/normalize_audit.py | 96 ++++++++++++------- 4 files changed, 73 insertions(+), 41 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index af2c247..dd0f312 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -50,7 +50,7 @@ When presenting the script's JSON findings to the merchant: **Supplement the script's findings with LLM-only checks:** -- **Semantic alias detection:** Values that are semantically equivalent but not in the known alias map (e.g., "Crimson" and "Red", "Burgundy" and "Wine"). Use your knowledge of colors, materials, and sizing to identify candidates. +- **Semantic alias detection:** Values that are semantically equivalent but not in the known alias map (e.g., "Crimson" and "Red", "Forest Green" and "Fir Green", "Cobalt" and "Royal Blue"). Use your knowledge of colors, materials, and sizing to identify candidates. - **Context-aware judgment:** Determine whether ambiguous values are sizes, colors, or something else based on the option name and product type. - **Brand voice alignment:** Consider whether the store's positioning suggests spelled-out sizes ("Extra Large") or abbreviations ("XL"). @@ -176,8 +176,14 @@ Leading or trailing spaces in any Option Name or Option Value cell. These are in Variants should appear in logical size order within each product. Check whether variant rows follow the expected sequence: -- **Apparel letter sizes:** XS, S, M, L, XL, XXL, 2XL, 3XL -- **Numeric sizes:** ascending order (28, 30, 32, 34...) +- **Apparel letter sizes:** XXS, XS, S, M, L, XL, XXL, 2XL, 3XL, 4XL, 5XL +- **Plus sizes:** 0X, 1X, 2X, 3X, 4X, 5X +- **Toddler sizes:** 2T, 3T, 4T, 5T, 6T +- **Youth sizes:** YXS, YS, YM, YL, YXL +- **Infant sizes:** NB, 0-3M, 3-6M, 6-9M, 6-12M, 12-18M, 18-24M +- **Petite sizes:** PS, PM, PL, PXL +- **Tall sizes:** TS, TM, TL, TXL +- **Numeric sizes:** ascending order (28, 30, 32... for waists; 7, 7.5, 8... for shoes) - **Named sizes (e.g., Small, Medium, Large):** map to their letter equivalents first Flag products where size-based variants are out of sequence. Note: this check only applies to Option columns that contain size values. diff --git a/skills/variant-option-normalizer/references/example-output.md b/skills/variant-option-normalizer/references/example-output.md index fce4b64..35815a0 100644 --- a/skills/variant-option-normalizer/references/example-output.md +++ b/skills/variant-option-normalizer/references/example-output.md @@ -19,7 +19,7 @@ python3 scripts/normalize_audit.py summitgear-products.csv --assets-dir assets/ "metadata": { "products_scanned": 4, "variants_scanned": 22, - "total_issues_found": 9, + "total_issues_found": 11, "has_variant_image_column": true, "csv_encoding": "utf-8-sig", "bom_detected": false @@ -27,7 +27,7 @@ python3 scripts/normalize_audit.py summitgear-products.csv --assets-dir assets/ } ``` -LLM analysis identified 2 additional semantic alias candidates not in the known alias map (Grey/Gray and XL/Extra Large), bringing the total presented to the merchant to 11. +The script detected all 11 issues, including both alias pairs (Grey/Gray via the color alias map, XL/Extra Large via the size alias map). ### Variant Option Audit diff --git a/skills/variant-option-normalizer/references/json-schema.md b/skills/variant-option-normalizer/references/json-schema.md index 1a39ea3..e8b49da 100644 --- a/skills/variant-option-normalizer/references/json-schema.md +++ b/skills/variant-option-normalizer/references/json-schema.md @@ -102,7 +102,7 @@ Each entry represents a product with out-of-order size variants. | size_option_field | string | Which column holds sizes, e.g., "Option1 Value" | | current_order | string[] | Size values in their current row order | | expected_order | string[] | Size values in the correct order | -| size_system | string | One of: "apparel_letter", "numeric", "compound", "unknown" | +| size_system | string | One of: "apparel_letter", "plus_size", "toddler", "youth", "infant", "petite", "tall", "numeric", "compound", "unknown" | ### issues.option_value_aliases diff --git a/skills/variant-option-normalizer/scripts/normalize_audit.py b/skills/variant-option-normalizer/scripts/normalize_audit.py index 6463766..120b408 100644 --- a/skills/variant-option-normalizer/scripts/normalize_audit.py +++ b/skills/variant-option-normalizer/scripts/normalize_audit.py @@ -23,7 +23,7 @@ from typing import Any -SCRIPT_VERSION = "1.0.0" +SCRIPT_VERSION = "1.1.0" # Shopify caps option values at 255 characters SHOPIFY_OPTION_VALUE_MAX_LENGTH = 255 @@ -565,13 +565,28 @@ def resolve_size_canonical(value: str, aliases: dict) -> str: return aliases.get(lower, value.strip()) -def detect_size_system(values: list[str], apparel_order: list[str]) -> str: - """Determine if values are apparel letters, numeric, compound, or unknown.""" - apparel_set = set(s.upper() for s in apparel_order) - canonical_values = [v.upper() for v in values] - - if all(v in apparel_set or v == "OS" for v in canonical_values): - return "apparel_letter" +def detect_size_system(values: list[str], size_config: dict) -> str: + """Determine the size system in use from the values present.""" + upper_values = [v.upper() for v in values] + + # Check each named order array in priority order + order_arrays = [ + ("apparel_letter_order", "apparel_letter"), + ("plus_order", "plus_size"), + ("toddler_order", "toddler"), + ("youth_order", "youth"), + ("infant_order", "infant"), + ("petite_order", "petite"), + ("tall_order", "tall"), + ] + for config_key, system_name in order_arrays: + order = size_config.get(config_key, []) + if not order: + continue + order_set = set(s.upper() for s in order) + # Allow OS (one size) alongside any named system + if all(v in order_set or v == "OS" for v in upper_values): + return system_name # Check numeric (including decimals like shoe sizes) numeric_pattern = re.compile(r"^\d+(\.\d+)?$") @@ -586,29 +601,41 @@ def detect_size_system(values: list[str], apparel_order: list[str]) -> str: return "unknown" -def sort_key_for_size(value: str, aliases: dict, apparel_order: list[str]) -> tuple: - """Return a sort key for a size value.""" +def sort_key_for_size(value: str, aliases: dict, size_config: dict) -> tuple: + """Return a sort key for a size value across all supported size systems.""" canonical = resolve_size_canonical(value, aliases) upper = canonical.upper() - # Check apparel ladder - if upper in apparel_order: - return (0, apparel_order.index(upper), 0) - - # Check numeric + # Each named order array gets its own bucket so systems don't intermix + order_arrays = [ + "apparel_letter_order", + "plus_order", + "toddler_order", + "youth_order", + "infant_order", + "petite_order", + "tall_order", + ] + for bucket, config_key in enumerate(order_arrays): + order = size_config.get(config_key, []) + order_upper = [s.upper() for s in order] + if upper in order_upper: + return (bucket, order_upper.index(upper), 0) + + # Numeric (shoe sizes, waist measurements entered as plain numbers) try: num = float(canonical) - return (1, num, 0) + return (len(order_arrays), num, 0) except ValueError: pass - # Check compound (waist x inseam) + # Compound (waist x inseam) match = re.match(r"^(\d+)\s*[x/]\s*(\d+)$", canonical, re.IGNORECASE) if match: - return (1, float(match.group(1)), float(match.group(2))) + return (len(order_arrays), float(match.group(1)), float(match.group(2))) - # Unknown: sort alphabetically at the end - return (2, 0, 0) + # Unknown: sort at the end, alphabetically within that bucket + return (len(order_arrays) + 1, 0, 0) def check_size_ordering(products: list[Product], size_config: dict) -> list[dict]: @@ -636,9 +663,9 @@ def check_size_ordering(products: list[Product], size_config: dict) -> list[dict if len(seen) <= 1: continue # Single size or OS: nothing to order - size_system = detect_size_system(seen, apparel_order) + size_system = detect_size_system(seen, size_config) - expected = sorted(seen, key=lambda v: sort_key_for_size(v, aliases, apparel_order)) + expected = sorted(seen, key=lambda v: sort_key_for_size(v, aliases, size_config)) if seen != expected: issues.append({ @@ -683,13 +710,16 @@ def load_alias_config(assets_dir: Path | None) -> dict: def check_option_value_aliases(products: list[Product], alias_config: dict, size_config: dict) -> list[dict]: issues: list[dict] = [] - # Build a bidirectional alias lookup from the color_aliases config - known_pairs: dict[str, str] = {} + # Build a canonical-group map: every value (including the canonical key itself) + # maps to its group's canonical form. Handles multi-variant groups like + # wine/maroon/burgundy correctly where a bidirectional pair map would not. + known_canonical: dict[str, str] = {} color_aliases = alias_config.get("color_aliases", {}) for canonical, variants in color_aliases.items(): + canonical_lower = canonical.lower() + known_canonical[canonical_lower] = canonical_lower for v in variants: - known_pairs[v.lower()] = canonical.lower() - known_pairs[canonical.lower()] = v.lower() + known_canonical[v.lower()] = canonical_lower # Build size canonical lookup for detecting size aliases like "Extra Large" / "XL" size_aliases = size_config.get("aliases", {}) @@ -732,12 +762,11 @@ def check_option_value_aliases(products: list[Product], alias_config: dict, size suggested = None # Strategy 1: Known color alias map - if val_a in known_pairs and known_pairs[val_a] == val_b: - detection_method = "known_alias_map" - confidence = "high" - elif val_b in known_pairs and known_pairs[val_b] == val_a: - detection_method = "known_alias_map" - confidence = "high" + # Two values alias if they resolve to the same canonical group root + if val_a in known_canonical and val_b in known_canonical: + if known_canonical[val_a] == known_canonical[val_b]: + detection_method = "known_alias_map" + confidence = "high" # Strategy 2: Size alias map (two values resolve to the same canonical size) if not detection_method and opt_name in size_option_names: @@ -1019,9 +1048,6 @@ def run_audit(csv_path: Path, assets_dir: Path | None) -> dict: size_config = load_size_config(assets_dir) alias_config = load_alias_config(assets_dir) - # Filter out default-title products for counting - active_products = [p for p in products if not is_default_title_product(p)] - whitespace = check_whitespace(products) case_issues = check_case_inconsistencies(products) duplicates = check_duplicate_variants(products) From 2db9cf5b2fc4d866dc3a0b900bfe60c8641581bc Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 14:38:08 +0600 Subject: [PATCH 14/24] feat: warn on post-normalization duplicates in Turn 2 confirmation --- .gitignore | 1 + skills/variant-option-normalizer/SKILL.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee4a943 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +skills/*/RandomRefs.md diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index dd0f312..90f175b 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -125,6 +125,7 @@ After the merchant reviews the audit, present the proposed normalization plan: 2. **Ambiguous choices.** Where multiple valid canonical forms exist (e.g., "Grey" vs "Gray", "Navy Blue" vs "Navy"), ask the merchant to choose. Do not assume a preference. 3. **Size ordering.** Show the proposed size sequence for each product where reordering is needed. 4. **Duplicates.** Confirm what action to take (flag only, remove second row, or merge). +5. **Post-normalization collision check.** Before presenting the plan, apply the proposed canonical mappings mentally across each product's variant rows. If any two distinct values on the same product would map to the same canonical form (e.g., both "XL" and "Extra Large" normalizing to "XL"), flag this explicitly in the plan. Do not silently merge these rows. Present both rows and ask the merchant to confirm which to keep, or whether they are intentionally distinct values that should not be merged. Wait for explicit confirmation before producing output. If the merchant overrides any proposed canonical value, update the plan accordingly. From 965113ac9720ce1d0754b23213dec3ae84afd365 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 14:55:56 +0600 Subject: [PATCH 15/24] feat: warn on post-normalization duplicates in Turn 2 confirmation --- skills/variant-option-normalizer/SKILL.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 90f175b..0c538c1 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -131,7 +131,15 @@ Wait for explicit confirmation before producing output. If the merchant override ### Turn 3: Produce Output -Generate two outputs: +Before writing any output, run this validation checklist. If any check fails, stop and report the issue to the merchant instead of producing a broken file. + +- **Row count:** Total rows in corrected CSV matches total rows in input (minus any duplicate rows explicitly approved for removal). +- **No emergent duplicates:** After applying all canonical mappings, no product has two variant rows with the same normalized Option1 + Option2 + Option3 combination unless the merchant explicitly approved a merge. +- **Required columns intact:** Handle, Title, Option1 Name, and Option1 Value are present and non-empty on every row where they were non-empty in the input. +- **No option cells cleared:** No Option Value cell that was populated in the input is now empty unless the merchant approved that change. +- **Change log count matches edits:** The number of change log entries accounts for every cell that was modified. + +If all checks pass, generate two outputs: 1. **Corrected CSV** as a downloadable file. Same column structure as the input. All header columns preserved. Changes applied inline. 2. **Change log** as a Markdown document using the output structure below. From cb492ee846dc459605549bc1cef01ad5e63c72fb Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:14:59 +0600 Subject: [PATCH 16/24] feat: propose fill-by-color image URLs in Turn 2 normalization plan --- skills/variant-option-normalizer/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 0c538c1..807ad4a 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -125,7 +125,8 @@ After the merchant reviews the audit, present the proposed normalization plan: 2. **Ambiguous choices.** Where multiple valid canonical forms exist (e.g., "Grey" vs "Gray", "Navy Blue" vs "Navy"), ask the merchant to choose. Do not assume a preference. 3. **Size ordering.** Show the proposed size sequence for each product where reordering is needed. 4. **Duplicates.** Confirm what action to take (flag only, remove second row, or merge). -5. **Post-normalization collision check.** Before presenting the plan, apply the proposed canonical mappings mentally across each product's variant rows. If any two distinct values on the same product would map to the same canonical form (e.g., both "XL" and "Extra Large" normalizing to "XL"), flag this explicitly in the plan. Do not silently merge these rows. Present both rows and ask the merchant to confirm which to keep, or whether they are intentionally distinct values that should not be merged. +5. **Missing variant images — fill-by-color proposal.** For each missing image flagged in the audit, check whether any sibling variant on the same product shares the same Option1 value (color) and already has a Variant Image URL. If so, include a proposed fill in the normalization plan table showing the source URL and which rows it would be copied to. Present this as a proposed action, not an automatic fix. If no sibling has an image for that color, leave it flagged as before. +6. **Post-normalization collision check.** Before presenting the plan, apply the proposed canonical mappings mentally across each product's variant rows. If any two distinct values on the same product would map to the same canonical form (e.g., both "XL" and "Extra Large" normalizing to "XL"), flag this explicitly in the plan. Do not silently merge these rows. Present both rows and ask the merchant to confirm which to keep, or whether they are intentionally distinct values that should not be merged. Wait for explicit confirmation before producing output. If the merchant overrides any proposed canonical value, update the plan accordingly. From f309353318211a27e04adc70bf821d43e05bfca5 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:17:09 +0600 Subject: [PATCH 17/24] feat: apply confidence framing to all 8 detection categories --- skills/variant-option-normalizer/SKILL.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 807ad4a..f234cc5 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -44,9 +44,17 @@ When presenting the script's JSON findings to the merchant: - `high` confidence: state as a finding ("These values are aliases and should be merged.") - `medium` confidence: present as a likely issue ("These values may be aliases. Can you confirm?") - `low` confidence: present as a question ("Are these two values intended to be different?") -3. **Handle warnings selectively.** Surface BOM detection, encoding issues, empty option values, and HTML entities in option values. Skip routine metadata like script version and timestamp. -4. **For handle/title drift:** Apply your own judgment about acceptable differences. The script flags all mismatches; filter out gendered suffix variations (e.g., dropping "Men's" or "Women's" from handles) before presenting to the merchant. -5. **For alias candidates with `same_product: true`:** These are almost certainly errors. Present them confidently. Cross-product aliases (`same_product: false`) may be intentional and should be framed as questions. +3. **Apply confidence framing to every category, not just aliases.** Use these rules to infer confidence for categories the script does not score: + - **Whitespace issues:** Always `high`. An invisible space is never intentional. + - **Case inconsistencies:** `high` when all variants of the value appear on the same product. `medium` when spread across multiple products (could be a supplier convention). + - **Size sequence ordering:** `high` when the sequence clearly violates a known size ladder (e.g., XL before S). `medium` when the size system is ambiguous or mixed. + - **Duplicate variants:** `high` when option combos are identical after normalization and prices/inventory match. `medium` when prices or inventory differ (could be intentional price variants). `low` when only one of the rows has a SKU (may be an import artifact). + - **Missing variant images:** `high` when sibling variants with the same color have images. `medium` when no sibling has an image for that color (could be a new color not yet photographed). + - **Option name inconsistencies:** `high` when the same synonym group appears across products (e.g., Color/Colour). `medium` when the names are semantically adjacent but not synonyms (e.g., Size/Dimensions). + - **Handle/title drift:** `low` by default. Only raise to `medium` when the drift is significant (e.g., handle is "blue-shirt" but title is "Red Jacket"). +4. **Handle warnings selectively.** Surface BOM detection, encoding issues, empty option values, and HTML entities in option values. Skip routine metadata like script version and timestamp. +5. **For handle/title drift:** Apply your own judgment about acceptable differences. The script flags all mismatches; filter out gendered suffix variations (e.g., dropping "Men's" or "Women's" from handles) before presenting to the merchant. +6. **For alias candidates with `same_product: true`:** These are almost certainly errors. Present them confidently. Cross-product aliases (`same_product: false`) may be intentional and should be framed as questions. **Supplement the script's findings with LLM-only checks:** From 13802bf2bfd49189ec3b07c1ce1b2d2ae4e81f27 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:18:08 +0600 Subject: [PATCH 18/24] feat: add confidence and needs-review columns to change log format --- skills/variant-option-normalizer/SKILL.md | 32 +++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index f234cc5..3c3d11d 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -251,38 +251,42 @@ The product handle should be a slugified version of the product title. Flag case ## Canonical Option Values Established -| Original Value | Canonical Value | Products Affected | Reason | -|---|---|---|---| +| Original Value | Canonical Value | Products Affected | Reason | Confidence | Needs Review | +|---|---|---|---|---|---| ## Issues Fixed ### Option Value Aliases Merged -[Details per merge with affected products] +| Original Value | Canonical Value | Products Affected | Confidence | Needs Review | +|---|---|---|---|---| ### Case Normalized -[Details per value with affected products] +| Original Value | Canonical Value | Products Affected | Confidence | Needs Review | +|---|---|---|---|---| ### Whitespace Removed -[Details per field with affected products] +| Field | Original Value | Product Handle | Confidence | Needs Review | +|---|---|---|---|---| ### Size Order Corrected -| Product Handle | Previous Order | Corrected Order | -|---|---|---| +| Product Handle | Previous Order | Corrected Order | Confidence | Needs Review | +|---|---|---|---|---| ### Duplicate Variants Flagged -| Product Handle | Option Combination | SKUs | Action Taken | -|---|---|---|---| +| Product Handle | Option Combination | SKUs | Action Taken | Confidence | Needs Review | +|---|---|---|---|---|---| ### Missing Variant Images Flagged -| Product Handle | SKU | Option Values | Note | -|---|---|---|---| +| Product Handle | SKU | Option Values | Note | Confidence | Needs Review | +|---|---|---|---|---|---| ### Option Names Standardized -[Details per name change with affected products] +| Original Name | Canonical Name | Products Affected | Confidence | Needs Review | +|---|---|---|---|---| ### Handle/Title Drift Flagged -| Handle | Title | Recommendation | -|---|---|---| +| Handle | Title | Recommendation | Confidence | Needs Review | +|---|---|---|---|---| --- From 4e7913163901e09cc7ef9a27fb1bb4cd8a3dde64 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:20:05 +0600 Subject: [PATCH 19/24] refactor: trim Detection Categories section, point to json-schema.md --- skills/variant-option-normalizer/SKILL.md | 60 +++-------------------- 1 file changed, 7 insertions(+), 53 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 3c3d11d..8cb9342 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -170,61 +170,15 @@ Edit specific changes in place when the merchant requests corrections. Do not re ## Detection Categories -Apply these checks in order. Each category is independent. +The audit script runs 8 checks: option value aliases, case inconsistencies, whitespace issues, size sequence ordering, duplicate variants, missing variant images, option name inconsistencies, and handle/title drift. For the full field-level specification of each check's JSON output, see [references/json-schema.md](references/json-schema.md). -### 1. Option Value Aliases - -Same color, size, or material referred to by different strings across variants or products. Common patterns: - -- Color: Gray/Grey, Charcoal/Charcoal Heather, Navy/Navy Blue -- Size: XL/Extra Large/X-Large, S/Small, M/Medium -- Material: variations in fiber content descriptions - -Compare values using normalized lowercase, whitespace-trimmed forms. Flag any pair of values within the same option name where one could be an alias of the other. When two values appear on the same product, note that explicitly (same-product aliases are almost certainly errors; cross-product aliases may be intentional). - -### 2. Case Inconsistencies - -Same value in different casing across variants: BLACK, Black, black. Compare within each option name across the full file. The most common casing form is the default canonical choice, but title case (e.g., "Black") is preferred when frequency is tied. - -### 3. Whitespace Issues - -Leading or trailing spaces in any Option Name or Option Value cell. These are invisible in spreadsheets but cause Shopify to treat "Black" and "Black " as different values, creating phantom variants. - -### 4. Size Sequence Ordering - -Variants should appear in logical size order within each product. Check whether variant rows follow the expected sequence: - -- **Apparel letter sizes:** XXS, XS, S, M, L, XL, XXL, 2XL, 3XL, 4XL, 5XL -- **Plus sizes:** 0X, 1X, 2X, 3X, 4X, 5X -- **Toddler sizes:** 2T, 3T, 4T, 5T, 6T -- **Youth sizes:** YXS, YS, YM, YL, YXL -- **Infant sizes:** NB, 0-3M, 3-6M, 6-9M, 6-12M, 12-18M, 18-24M -- **Petite sizes:** PS, PM, PL, PXL -- **Tall sizes:** TS, TM, TL, TXL -- **Numeric sizes:** ascending order (28, 30, 32... for waists; 7, 7.5, 8... for shoes) -- **Named sizes (e.g., Small, Medium, Large):** map to their letter equivalents first - -Flag products where size-based variants are out of sequence. Note: this check only applies to Option columns that contain size values. - -When reordering rows, keep product-level metadata (Title, Body, Vendor, Tags, Image Src, SEO fields, Published, Status) on the first row of each product handle group. If a row moves into the first position, transfer those fields to it and clear them from the displaced row. - -### 5. Duplicate Variants - -Two or more rows on the same product handle with identical Option1 + Option2 + Option3 value combinations. Compare after normalizing case and trimming whitespace to catch duplicates hidden by inconsistencies. - -If duplicates have different prices or inventory quantities, flag them for manual review rather than auto-merging. - -### 6. Missing Variant Images - -The `Variant Image` column is empty on a variant row while other variants on the same product have images. This check only applies when the input CSV includes the Variant Image column. Note: this check detects missing URLs, not broken URLs. Populated image URLs are not validated. - -### 7. Option Name Inconsistencies - -The same dimension called different things across products: Size vs Dimensions, Color vs Colour, Material vs Fabric. Compare all Option1/Option2/Option3 Name values across the file. Flag any near-matches. - -### 8. Handle/Title Drift +Behavioral notes for interpreting results: -The product handle should be a slugified version of the product title. Flag cases where the handle does not match what the title would produce (lowercase, hyphens for spaces, apostrophes removed, consecutive hyphens collapsed, no other special characters). Minor differences (e.g., dropping "Men's" or "Women's" from the handle) are acceptable and should not be flagged. +- **Size sequence:** Only applies to Option columns the script classifies as containing size values. Read the `size_system` field in size ordering findings and use it to constrain your alias proposals — do not suggest apparel letter sizes for a product with an infant or numeric size system. +- **Missing variant images:** Only runs when the input CSV includes the `Variant Image` column. Detects missing URLs only — does not validate whether populated URLs resolve. +- **Duplicate variants:** Comparison is after normalizing case and trimming whitespace, so duplicates hidden by casing differences are caught. If duplicates have different prices or inventory, flag for manual review rather than auto-merging. +- **Handle/title drift:** The script flags all mismatches. Apply your own judgment — filter out acceptable differences (gendered suffixes, minor punctuation) before presenting to the merchant. +- **Row reordering for size fixes:** When reordering variant rows, keep product-level metadata (Title, Body, Vendor, Tags, Image Src, SEO fields, Published, Status) on the first row of each product handle group. If a row moves into the first position, transfer those fields to it and clear them from the displaced row. --- From 892d4fc92462d97058d1f827d02dfb79559ff059 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:21:40 +0600 Subject: [PATCH 20/24] feat: offer approved_mapping.json save for future re-use in Turn 3 --- skills/variant-option-normalizer/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 8cb9342..8d6b891 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -157,8 +157,9 @@ After generating both outputs, ask the merchant: - Where to save the corrected CSV (suggest a filename based on the input, e.g. `shopify-products-normalized.csv` in the same directory). - Where to save the change log (suggest a filename alongside the CSV, e.g. `shopify-products-normalized-changelog.md`). +- Whether to save the approved canonical mapping as `approved_mapping.json` alongside the CSV. This file captures every original-to-canonical value mapping confirmed in Turn 2. On future exports from the same store, it can be reapplied directly to skip re-analysis for values already resolved. -Write both files to the confirmed paths before closing out the turn. +Write all confirmed files to their confirmed paths before closing out the turn. Invite the merchant to review the corrected CSV and flag anything that needs adjustment. From 40cb511ca49940a5c8153980d63088ff9e82c1f8 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:24:55 +0600 Subject: [PATCH 21/24] feat: detect incomplete variant matrix in audit script warnings --- .../scripts/normalize_audit.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/skills/variant-option-normalizer/scripts/normalize_audit.py b/skills/variant-option-normalizer/scripts/normalize_audit.py index 120b408..c6bbfbe 100644 --- a/skills/variant-option-normalizer/scripts/normalize_audit.py +++ b/skills/variant-option-normalizer/scripts/normalize_audit.py @@ -12,6 +12,7 @@ import argparse import csv import html +import itertools import json import re import sys @@ -1034,6 +1035,57 @@ def check_warnings(products: list[Product], columns: list[str], metadata: dict) "details": {"handle": product.handle}, }) + # Variant count mismatch (incomplete option matrix) + for product in products: + if is_default_title_product(product): + continue + # Collect distinct values per option slot across all rows + slot_values: list[set[str]] = [set(), set(), set()] + for row in product.rows: + for i, (_, name, value) in enumerate([( + "Option1", row.option1_name, row.option1_value, + ), ( + "Option2", row.option2_name, row.option2_value, + ), ( + "Option3", row.option3_name, row.option3_value, + )]): + if name.strip() and value.strip(): + slot_values[i].add(value.strip()) + # Only flag when at least 2 option slots are in use + active_slots = [v for v in slot_values if v] + if len(active_slots) < 2: + continue + expected = 1 + for s in active_slots: + expected *= len(s) + actual = len(product.rows) + if actual < expected: + # Build the list of all expected combinations and subtract observed ones + observed_combos: set[tuple[str, ...]] = set() + for row in product.rows: + combo = tuple( + v.strip() for v in [row.option1_value, row.option2_value, row.option3_value] + if v.strip() + ) + observed_combos.add(combo) + # Generate full expected matrix + full_matrix = list(itertools.product(*active_slots)) + missing = [list(c) for c in full_matrix if c not in observed_combos] + warnings.append({ + "code": "variant_count_mismatch", + "message": ( + f"Product '{product.handle}' has {actual} variant rows but the option matrix " + f"suggests {expected} combinations. {len(missing)} combination(s) appear missing." + ), + "details": { + "handle": product.handle, + "expected_count": expected, + "actual_count": actual, + "missing_count": len(missing), + "missing_combinations": missing[:20], # cap at 20 to avoid huge output + }, + }) + return warnings From a1496b950ee4543a9258438ac33f18d85455d673 Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:26:19 +0600 Subject: [PATCH 22/24] feat: tighten LLM semantic check instructions to reduce over-normalization --- skills/variant-option-normalizer/SKILL.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 8d6b891..5d28477 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -58,9 +58,10 @@ When presenting the script's JSON findings to the merchant: **Supplement the script's findings with LLM-only checks:** -- **Semantic alias detection:** Values that are semantically equivalent but not in the known alias map (e.g., "Crimson" and "Red", "Forest Green" and "Fir Green", "Cobalt" and "Royal Blue"). Use your knowledge of colors, materials, and sizing to identify candidates. -- **Context-aware judgment:** Determine whether ambiguous values are sizes, colors, or something else based on the option name and product type. -- **Brand voice alignment:** Consider whether the store's positioning suggests spelled-out sizes ("Extra Large") or abbreviations ("XL"). +- **Semantic alias detection:** Look for values that are semantically equivalent but not in the script's known alias map. Scope this check to values within the same `option_name` group only — do not compare across option slots. Examples: "Crimson" and "Cherry Red" within a Color option, "Cobalt" and "Royal Blue" within a Color option. Do not propose merging values that are close but intentionally distinct (e.g., "Fir Green" and "Forest Green" may be separate SKUs — flag as a question, not a finding). Never merge across different option names (e.g., do not conflate a size value with a color value even if they share a word). +- **Locale and unit awareness:** Values that look like aliases may actually be distinct. "US 10" and "UK 9" are different shoe sizes and must never be merged. "S (AU)" and "S" may differ. When a value includes a locale or unit qualifier, treat it as distinct unless the merchant confirms otherwise. +- **Context-aware judgment:** Determine whether an ambiguous value is a size, color, or material based on the `option_name` column and the surrounding values in that product. Read the `size_system` field from any size ordering findings to constrain size alias proposals — do not suggest apparel letter-size canonicals for infant or numeric size products. +- **Brand voice alignment:** Look at the dominant pattern across the file. If most size values are spelled out ("Small", "Medium", "Large"), propose that form as canonical rather than abbreviations. If most are abbreviated ("S", "M", "L"), propose abbreviations. Do not override a consistent intentional style in favor of a generic standard. --- From 3e18bad1a1597054311ee305113d50919b59386f Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:27:29 +0600 Subject: [PATCH 23/24] feat: add category slug column to canonical values table in change log --- skills/variant-option-normalizer/SKILL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index 5d28477..d69dd8c 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -207,8 +207,10 @@ Behavioral notes for interpreting results: ## Canonical Option Values Established -| Original Value | Canonical Value | Products Affected | Reason | Confidence | Needs Review | -|---|---|---|---|---|---| +| Original Value | Canonical Value | Products Affected | Category | Reason | Confidence | Needs Review | +|---|---|---|---|---|---|---| + +Valid category values: `synonym_normalized`, `case_normalized`, `whitespace_removed`, `size_reordered`, `duplicate_flagged`, `image_flagged`, `option_name_standardized`, `handle_drift_flagged`. ## Issues Fixed From f4f66713f7a7a3a205be9ab0a3b32b61dd1bb81e Mon Sep 17 00:00:00 2001 From: Raqib Abdullah Date: Thu, 9 Apr 2026 15:28:46 +0600 Subject: [PATCH 24/24] feat: add error handling section for malformed CSV and structural failures --- skills/variant-option-normalizer/SKILL.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/skills/variant-option-normalizer/SKILL.md b/skills/variant-option-normalizer/SKILL.md index d69dd8c..e201fd7 100644 --- a/skills/variant-option-normalizer/SKILL.md +++ b/skills/variant-option-normalizer/SKILL.md @@ -297,6 +297,21 @@ The skill works with a single product. Cross-product checks (option name consist --- +## Error Handling + +Stop and report clearly to the merchant rather than proceeding with bad data. Do not attempt normalization on a file you cannot parse reliably. + +| Error | What to do | +|---|---| +| Script exits with code 1 (fatal error) | Report the script error message verbatim. Fall back to LLM-only analysis if the merchant wants to continue. Note in the change log that the audit was performed without the script. | +| Missing required columns (Handle, Title, Option1 Name, Option1 Value) | Tell the merchant which columns are missing. Ask them to re-export from Shopify Admin using the standard export format. Do not proceed. | +| Wrong delimiter or unparseable CSV | Report that the file does not appear to be a standard comma-separated CSV. Mention that Shopify exports use UTF-8 encoding with comma delimiters. Ask the merchant to re-export or check if the file was opened and re-saved by Excel (which can change delimiters). | +| Corrupt rows (column count mismatch) | Report the row numbers that appear malformed. Ask the merchant to inspect those rows in a spreadsheet before re-uploading. Do not skip silently. | +| All products are default-title products | Tell the merchant the file contains only single-variant products (no option columns in use). There is nothing to normalize. Confirm whether they uploaded the correct file. | +| Unrecoverable ambiguity (e.g., two conflicting canonical values both supported by the data) | Present both options to the merchant with the evidence for each. Do not guess. Wait for explicit direction before proceeding. | + +--- + ## Closing Once the merchant approves the corrected CSV, note that it is ready for Shopify bulk import via Settings > Import in the Shopify admin. Include these reminders: