fix(products): conservative dialect detection — canonical-distinctive columns veto Shopify (SLICE-8 review)
Addresses adversarial-review findings: a canonical file carrying a stray Shopify-signature name (e.g. SEO Title) no longer misdetects as Shopify and silently drops canonical Type / corrupts the weight unit (kg->g). A canonical-distinctive column (Description/Variant Cost/Variant Weight/Google Product Category/Variant Volume/Tax ID/Position) now vetoes detection, so detection leans conservative (under-detection warns; over-detection corrupts). Also closes the dual-named-column shadow (Body (HTML)+Description) and the stale-weight-unit-on-clear case. Tests cover all three. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,9 +73,13 @@ def parse_csv(data: bytes) -> ParsedFile:
|
||||
c: (raw[col_index[c]].strip() if col_index[c] < len(raw) else "")
|
||||
for c in col_index
|
||||
}
|
||||
# Shopify grams carry an implicit unit; canonical needs it explicit (§6.5.1).
|
||||
if dialect == "shopify" and cells.get("Variant Weight"):
|
||||
cells["Variant Weight Unit"] = "g"
|
||||
# Shopify grams carry an implicit unit; canonical needs it explicit. In a
|
||||
# Shopify file "Variant Weight" can only come from the Variant Grams rename
|
||||
# (a canonical-named Variant Weight would have vetoed Shopify detection), so
|
||||
# weight and unit move together: a value -> "g"; a cleared grams (present-but-
|
||||
# empty) clears the unit too, never leaving a stale unit (§6.5.1).
|
||||
if dialect == "shopify" and "Variant Weight" in cells:
|
||||
cells["Variant Weight Unit"] = "g" if cells["Variant Weight"] else ""
|
||||
rows.append(Row(line_number=reader.line_num, cells=cells))
|
||||
except csv.Error:
|
||||
raise FileRejected("not_csv", "This file isn't readable as CSV.") from None
|
||||
|
||||
@@ -38,12 +38,36 @@ _SIGNATURE: frozenset[str] = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Canonical-distinctive columns — names Shopify renames away (so a real Shopify export
|
||||
# never carries them) plus canonical-only columns Shopify has no equivalent for. Their
|
||||
# presence proves the file is canonical and VETOES Shopify detection: detection must
|
||||
# lean conservative, because under-detection is safe (Shopify-named columns are warned
|
||||
# as not-imported) while over-detection corrupts (canonical Type / weight-unit are
|
||||
# dropped). This closes the misdetection hazard — a canonical file that merely contains
|
||||
# a stray signature-named column (e.g. `SEO Title`) is never misread as Shopify (§7.4).
|
||||
_CANONICAL_SIGNATURE: frozenset[str] = frozenset(
|
||||
{
|
||||
"Description",
|
||||
"Variant Cost",
|
||||
"Variant Weight",
|
||||
"Google Product Category",
|
||||
"Variant Volume",
|
||||
"Variant Volume Unit",
|
||||
"Variant Tax ID 1",
|
||||
"Variant Tax ID 2",
|
||||
"Variant Position",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def is_shopify_header(header: list[str]) -> bool:
|
||||
"""True iff the header carries a Shopify-only signature column. Otherwise the
|
||||
file is treated as canonical — shared columns map identically, so an ambiguous
|
||||
header never misparses (§7.4)."""
|
||||
"""True iff the header carries a Shopify-only signature column AND no
|
||||
canonical-distinctive column. A canonical signal vetoes Shopify detection so an
|
||||
ambiguous or hybrid header is treated as canonical — where Shopify-named columns
|
||||
are warned as not-imported rather than silently remapped (never misparses, §7.4)."""
|
||||
cols = {h.strip() for h in header}
|
||||
if cols & _CANONICAL_SIGNATURE:
|
||||
return False
|
||||
if cols & _SIGNATURE:
|
||||
return True
|
||||
return any(c.startswith("Google Shopping / ") for c in cols)
|
||||
|
||||
Reference in New Issue
Block a user