From 6459e344800768a0a9597145cccea096cd41864f Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Fri, 12 Jun 2026 02:24:44 -0700 Subject: [PATCH] docs(products): DOC-4 dialect-adapter notes (SLICE-8) Co-Authored-By: Claude Fable 5 --- docs/products-domain.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/products-domain.md b/docs/products-domain.md index ce3f8f3..65a6d05 100644 --- a/docs/products-domain.md +++ b/docs/products-domain.md @@ -60,6 +60,42 @@ Option names live both in `CanonicalProduct.option_names` (the values) and in `fields{}` (the file-presence marker the diff needs for the absent-vs-clear distinction). +## Dialects (INV-17, SLICE-8) + +A file is normalized to canonical names **at the codec boundary**, so every stage +downstream of `parse_csv` (validate → diff → apply) is dialect-agnostic — it only +ever sees canonical cells. Two dialects exist: `canonical` and `shopify`. + +`dialect_shopify.py` is the Shopify adapter and the source of truth for the +mapping (mirrored by `tests/test_products_dialect_shopify.py` and +`tests/fixtures/shopify-export.csv`): + +- **Detection** — `is_shopify_header()` returns `shopify` iff the header carries a + Shopify-only *signature* column (`Body (HTML)`, `Variant Grams`, `Cost per item`, + `Gift Card`, a `Google Shopping / …` column, …). Everything else is `canonical`. + Detection is by signature, not an exact header set, so extra market/region columns + don't defeat it; an ambiguous header (only columns common to both dialects) falls + through to `canonical`, which maps those shared columns identically — so it never + misparses (§7.4). +- **Mapping** — `map_shopify_header(header)` returns `(mapped, not_imported)`: + `mapped[i]` is the canonical name for column `i` (or `None` when it has no + canonical home), and `not_imported` is the warning list surfaced as the draft's + `unknown_columns`. The rule per column, in order: in `SHOPIFY_RENAME` → + canonical name; in `SHOPIFY_DROP` (`Type`, `Variant Weight Unit`) → not imported; + in `KNOWN_COLUMNS` → pass through; else → not imported (this last branch absorbs + the unbounded `… / ` price columns without enumerating them). +- **The two name-collision overrides** are why `SHOPIFY_DROP` exists: Shopify's + `Type` is a free-text category, but canonical `Type` is *structural* + (`standalone`/kits), so it is warned rather than folded in; and Shopify's + `Variant Weight Unit` is dropped because the `Variant Grams` → `Variant Weight` + rename is the weight source. +- **The one value transform** lives in `parse_csv`: when a Shopify row has a mapped + `Variant Weight` (from `Variant Grams`), the codec synthesizes + `Variant Weight Unit = "g"`, since Shopify grams are unitless integers. + +`detect_dialect()` in `codec.py` is the INV-17 seam; a new dialect is a new adapter +module plus a branch there, with no change to validate/diff/apply. + ## Error granularity `validate.py` never raises on a row problem: every violation is recorded as a