[object Object]

← back to Reid Witlin Onboarding

TK-11256: fix Reid Witlin/Architectural Fabrics onboarder dropping description on the floor

97382b28fe63b1f04fa413e71e79b287c20ab8b9 · 2026-09-04 11:42:30 -0700 · Steve Abrams

Root cause of dw-five-field-canary cadence-regression FAIL (1005 recent products,
2026-09-03 batch): build_batch2.py never SELECTed rwltd_catalog.description/
ai_description and never emitted a description field in the CSV; create2.py never
sent descriptionHtml to Shopify at all. Also: activate_batch.py had no
description-present gate before flipping DRAFT->ACTIVE, unlike every other DW
onboarder's go-live.mjs (which holds as draft on empty descriptionHtml).

Fixes (code-only, no data/Shopify writes fired):
- build_batch2.py: SELECT description, ai_description in both pool queries; rec_common()
  now emits description (description || ai_description, may still be empty if the
  scraper never captured either -- separate data gap, not this bug).
- create2.py: sends descriptionHtml when row['description'] is non-empty.
- activate_batch.py: holds any product with empty descriptionHtml as DRAFT (writes
  held-no-description.json) instead of activating it -- protects future runs against
  repeating this regression.

Note: the single-Sample-variant/quote-only shape is NOT a bug -- Steve explicitly
approved activating this batch in that shape (git a5b4a18), matching the precedent of
190 prior same-shape ACTIVE Reid Witlin products. Not touched.

Already-live 1,005 products still missing descriptions is a Shopify write -- gated,
see pending-approval memo.

Files touched

Diff

commit 97382b28fe63b1f04fa413e71e79b287c20ab8b9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 4 11:42:30 2026 -0700

    TK-11256: fix Reid Witlin/Architectural Fabrics onboarder dropping description on the floor
    
    Root cause of dw-five-field-canary cadence-regression FAIL (1005 recent products,
    2026-09-03 batch): build_batch2.py never SELECTed rwltd_catalog.description/
    ai_description and never emitted a description field in the CSV; create2.py never
    sent descriptionHtml to Shopify at all. Also: activate_batch.py had no
    description-present gate before flipping DRAFT->ACTIVE, unlike every other DW
    onboarder's go-live.mjs (which holds as draft on empty descriptionHtml).
    
    Fixes (code-only, no data/Shopify writes fired):
    - build_batch2.py: SELECT description, ai_description in both pool queries; rec_common()
      now emits description (description || ai_description, may still be empty if the
      scraper never captured either -- separate data gap, not this bug).
    - create2.py: sends descriptionHtml when row['description'] is non-empty.
    - activate_batch.py: holds any product with empty descriptionHtml as DRAFT (writes
      held-no-description.json) instead of activating it -- protects future runs against
      repeating this regression.
    
    Note: the single-Sample-variant/quote-only shape is NOT a bug -- Steve explicitly
    approved activating this batch in that shape (git a5b4a18), matching the precedent of
    190 prior same-shape ACTIVE Reid Witlin products. Not touched.
    
    Already-live 1,005 products still missing descriptions is a Shopify write -- gated,
    see pending-approval memo.
---
 activate_batch.py | 17 ++++++++++++++++-
 build_batch2.py   | 11 +++++++++--
 create2.py        |  8 ++++++++
 3 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/activate_batch.py b/activate_batch.py
index 01001db..c94633b 100644
--- a/activate_batch.py
+++ b/activate_batch.py
@@ -48,7 +48,7 @@ LIST_Q = """
 query($cursor: String) {
   products(first: 250, after: $cursor, query: "vendor:'Architectural Fabrics' status:draft") {
     pageInfo { hasNextPage endCursor }
-    nodes { id handle title }
+    nodes { id handle title descriptionHtml }
   }
 }"""
 
@@ -73,6 +73,21 @@ def fetch_all():
 
 def main():
     products = fetch_all()
+    # GATE (TK-11256, 2026-09-04): every other DW onboarder's go-live.mjs holds a product
+    # as DRAFT when descriptionHtml is empty (see maharam/osborne/knoll-onboard go-live.mjs
+    # "even if create-drafts shipped an empty descriptionHtml, this HOLDS the product as
+    # draft"). This script had no such check -- the 2026-09-03 1,005-item activation shipped
+    # every product with no description and tripped dw-five-field-canary. This does NOT
+    # touch anything already live; it only protects future runs of this script (e.g. the
+    # 275 rows still pending onboarding).
+    held_no_desc = [p for p in products
+                    if not (p.get("descriptionHtml") or "").replace("<p>", "").replace("</p>", "").strip()]
+    if held_no_desc:
+        print(f"HOLDING {len(held_no_desc)}/{len(products)} as DRAFT (no descriptionHtml) -- "
+              f"see held-no-description.json")
+        json.dump([{"id": p["id"], "handle": p["handle"]} for p in held_no_desc],
+                   open("held-no-description.json", "w"), indent=2)
+    products = [p for p in products if p not in held_no_desc]
     if LIMIT:
         products = products[:LIMIT]
     print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: {len(products)} products "
diff --git a/build_batch2.py b/build_batch2.py
index 4345caf..49cef7c 100644
--- a/build_batch2.py
+++ b/build_batch2.py
@@ -115,6 +115,13 @@ def rec_common(mfr, dw_sku, pat, town, colorway_disp, product_type_, img, conf,
         "repeat": row.get("spec_repeat") or "",
         "style": ", ".join(row["ai_styles"]) if isinstance(row.get("ai_styles"), list) else "",
         "tags": " | ".join(build_tags(row, colorway_disp.lower().replace(' ', '-'), town)),
+        # FIX (TK-11256, 2026-09-04): rwltd_catalog.description/ai_description were being
+        # queried nowhere and dropped on the floor here, so create2.py never had a body_html
+        # to send -- every product this pipeline created shipped with NO description (the
+        # dw-five-field-canary cadence-regression FAIL on the 1005-item 2026-09-03 batch).
+        # Prefer human-authored description, fall back to ai_description. May still be empty
+        # for rows where the scraper never captured either -- that's a data gap, not this bug.
+        "description": (row.get("description") or row.get("ai_description") or "").strip(),
         "pool": "A-reconfirmed-old" if row.get("_pool") == "A" else "B-new-gap",
     }
 
@@ -126,7 +133,7 @@ def main():
       SELECT mfr_sku, dw_sku, pattern_name, color_name, virginia_town, spec_type,
              spec_width, spec_actual_width, spec_content, spec_repeat, spec_flamecode,
              spec_origin, spec_cleaning, spec_finish, spec_abrasion,
-             gallery_images, ai_tags, ai_styles
+             gallery_images, ai_tags, ai_styles, description, ai_description
       FROM rwltd_catalog
       WHERE dw_sku IS NOT NULL AND dw_sku <> ''
         AND (shopify_product_id IS NULL OR shopify_product_id='')
@@ -155,7 +162,7 @@ def main():
       SELECT mfr_sku, pattern_name, color_name, virginia_town, spec_type,
              spec_width, spec_actual_width, spec_content, spec_repeat, spec_flamecode,
              spec_origin, spec_cleaning, spec_finish, spec_abrasion,
-             gallery_images, ai_tags, ai_styles, image_url
+             gallery_images, ai_tags, ai_styles, image_url, description, ai_description
       FROM rwltd_catalog
       WHERE (dw_sku IS NULL OR dw_sku='')
         AND (shopify_product_id IS NULL OR shopify_product_id='')
diff --git a/create2.py b/create2.py
index 1c66c6c..ea9ba77 100644
--- a/create2.py
+++ b/create2.py
@@ -89,6 +89,12 @@ def build_input(row):
         "productType": row["product_type"],
         "status": "DRAFT",
         "tags": [t.strip() for t in row["tags"].split("|") if t.strip()],
+        # FIX (TK-11256, 2026-09-04): body_html was never sent -- this is the #2 cause
+        # (with the sample-only-variant shape being by-design/quote-only) of the
+        # dw-five-field-canary FAIL on the 2026-09-03 1,005-item batch. build_batch2.py
+        # now carries description/ai_description through the CSV as row["description"];
+        # send it as body_html when present. Still empty for rows where the scraper never
+        # captured a description at all (rwltd_catalog gap, not this script's fault).
         "productOptions": [{"name": "Title", "values": [{"name": "Sample"}]}],
         "variants": [{
             "optionValues": [{"optionName": "Title", "name": "Sample"}],
@@ -97,6 +103,8 @@ def build_input(row):
             "inventoryItem": {"tracked": False},
         }],
     }
+    if row.get("description") and row["description"].strip():
+        inp["descriptionHtml"] = row["description"].strip()
     if metafields:
         inp["metafields"] = metafields
     if row.get("image_url"):

← 4d7b594 auto-data-snapshot: 2026-09-03T17:39:42 (1 data files) — rwl  ·  back to Reid Witlin Onboarding  ·  backfill-rwltd-mfr: namespace-scoped idempotency + HTTP stat bb0a80a →