SLICE-8: Shopify dialect — detect/map Shopify export at the codec boundary + DOC-2/3 (SD-0002 §7.2) #32

Merged
ben.stull merged 11 commits from slice-8-shopify-dialect into main 2026-06-12 09:37:38 +00:00
Showing only changes of commit 6459e34480 - Show all commits
+36
View File
@@ -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 `fields{}` (the file-presence marker the diff needs for the absent-vs-clear
distinction). 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 `… / <Market>` 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 ## Error granularity
`validate.py` never raises on a row problem: every violation is recorded as a `validate.py` never raises on a row problem: every violation is recorded as a