[object Object]

← back to Roll On Fabric Fix

Roll-on-fabric fix prep — dry-run metafield-delete tooling for 45 Mulberry (no live writes)

6998e0be50ec9b569268456911c440f6ffb64359 · 2026-08-01 22:10:48 -0700 · Steve Abrams

Files touched

Diff

commit 6998e0be50ec9b569268456911c440f6ffb64359
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 22:10:48 2026 -0700

    Roll-on-fabric fix prep — dry-run metafield-delete tooling for 45 Mulberry (no live writes)
---
 .gitignore             |   5 +
 README.md              |  21 ++++
 data/mulberry-nums.txt |  45 +++++++
 data/worklist.json     | 317 +++++++++++++++++++++++++++++++++++++++++++++++++
 fix.py                 |  59 +++++++++
 reverse.py             |  27 +++++
 6 files changed, 474 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff2422c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cf58535
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# Roll-on-fabric fix (PREP — nothing live)
+
+45 Mulberry fabrics carry a stray legacy `global.unit_of_measure = "Full Roll"` metafield alongside
+the correct `global.Unit of measure = "YARD"`. This deletes the stray key (canonical YARD stays),
+which also stops the roll-on-fabric canary's daily false-FAIL noise. Built 2026-08-01.
+
+## Work-list (`data/worklist.json`) — 45, each pre-verified
+Legacy key present AND canonical YARD present (safe to delete). Carries mf_id + full legacy def for undo.
+
+## Fix
+- `fix.py` — DELETEs the legacy metafield per product. DRY-RUN default; live needs `--apply` +
+  `ROLLFIX_CONFIRM=YES`; `--canary N`. `reverse.py` re-creates the exact legacy metafield (undo).
+
+## Note
+This removes the LIVE stray keys (~45 Mulberry). The ~407 Brunschwig the canary also flags are
+mirror-stale FALSE POSITIVES (already clean live) — do NOT touch them; they clear when the mirror
+sync is fixed (replace-not-merge, separate code task). Memo:
+`~/.claude/yolo-queue/pending-approval/2026-08-01-roll-on-fabric-canary-decomposed.md`
+
+## Go-live (Steve-gated)
+`ROLLFIX_CONFIRM=YES python3 fix.py --apply --canary 5` → re-run the canary → widen to 45.
diff --git a/data/mulberry-nums.txt b/data/mulberry-nums.txt
new file mode 100644
index 0000000..0ef52b9
--- /dev/null
+++ b/data/mulberry-nums.txt
@@ -0,0 +1,45 @@
+7422895620147
+7422896570419
+7422896603187
+7422896668723
+7422896767027
+7422896799795
+7422896898099
+7422897029171
+7422897094707
+7422897651763
+7422897750067
+7422897782835
+7422897815603
+7422897848371
+7422897881139
+7422897913907
+7422897946675
+7422898339891
+7422898372659
+7422898438195
+7422898503731
+7422898536499
+7422898569267
+7422898995251
+7422899552307
+7422899585075
+7422899617843
+7422899650611
+7422899683379
+7422899781683
+7422901190707
+7422901256243
+7422901289011
+7422901321779
+7422901354547
+7422901420083
+7422901485619
+7422901583923
+7422901682227
+7422901747763
+7422901878835
+7422901944371
+7422902009907
+7422902075443
+7422903779379
diff --git a/data/worklist.json b/data/worklist.json
new file mode 100644
index 0000000..6be021e
--- /dev/null
+++ b/data/worklist.json
@@ -0,0 +1,317 @@
+[
+{
+"num": "7422895620147",
+"mf_id": 35874238660659,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896570419",
+"mf_id": 35874240167987,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896603187",
+"mf_id": 35874240299059,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896668723",
+"mf_id": 35874240626739,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896767027",
+"mf_id": 35874240790579,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896799795",
+"mf_id": 35874240954419,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422896898099",
+"mf_id": 35874241183795,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897029171",
+"mf_id": 35874241413171,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897094707",
+"mf_id": 35874241871923,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897651763",
+"mf_id": 35874243412019,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897750067",
+"mf_id": 35874243575859,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897782835",
+"mf_id": 35874243706931,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897815603",
+"mf_id": 35874243838003,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897848371",
+"mf_id": 35874244034611,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897881139",
+"mf_id": 35874244526131,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897913907",
+"mf_id": 35874245836851,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422897946675",
+"mf_id": 35874246033459,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898339891",
+"mf_id": 35874246393907,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898372659",
+"mf_id": 35874246590515,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898438195",
+"mf_id": 35874246721587,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898503731",
+"mf_id": 35874246918195,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898536499",
+"mf_id": 35874248294451,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898569267",
+"mf_id": 35874248556595,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422898995251",
+"mf_id": 35874249179187,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899552307",
+"mf_id": 35874250489907,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899585075",
+"mf_id": 35874250719283,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899617843",
+"mf_id": 35874251276339,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899650611",
+"mf_id": 35874251472947,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899683379",
+"mf_id": 35874251636787,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422899781683",
+"mf_id": 35874251866163,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901190707",
+"mf_id": 35874253406259,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901256243",
+"mf_id": 35874253602867,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901289011",
+"mf_id": 35874253799475,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901321779",
+"mf_id": 35874254520371,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901354547",
+"mf_id": 35874255274035,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901420083",
+"mf_id": 35874255437875,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901485619",
+"mf_id": 35874255732787,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901583923",
+"mf_id": 35874255962163,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901682227",
+"mf_id": 35874256715827,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901747763",
+"mf_id": 35874256846899,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901878835",
+"mf_id": 35874258124851,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422901944371",
+"mf_id": 35874258321459,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422902009907",
+"mf_id": 35874258518067,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422902075443",
+"mf_id": 35874258681907,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+},
+{
+"num": "7422903779379",
+"mf_id": 35874258845747,
+"legacy_value": "Full Roll",
+"legacy_type": "single_line_text_field",
+"canon": "YARD"
+}
+]
\ No newline at end of file
diff --git a/fix.py b/fix.py
new file mode 100644
index 0000000..a45a2db
--- /dev/null
+++ b/fix.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+"""
+Roll-on-fabric fix.  PREP / DRY-RUN BY DEFAULT.
+
+45 Mulberry fabrics carry a stray legacy metafield  global.unit_of_measure = "Full Roll"
+alongside the correct  global.Unit of measure = "YARD".  This DELETES the stray legacy key
+(the canonical YARD stays), which also de-noises the roll-on-fabric canary.
+
+Each target was pre-verified: legacy key present AND canonical YARD present (data/worklist.json).
+
+SAFE BY CONSTRUCTION:
+  * DRY-RUN default. Prints the plan, ZERO Shopify writes.
+  * Live requires BOTH:  --apply  AND  env ROLLFIX_CONFIRM=YES
+  * --canary N limits to first N.  ~0.4s paced.
+  * Fully reversible: reverse.py re-creates the exact legacy metafield from the worklist.
+  * The REAL root-cause fix (mirror sync replace-not-merge to prune deleted keys) is separate
+    and code-side; this only removes the live stray keys.
+
+Usage:
+  python3 fix.py                                    # DRY-RUN all 45 (no writes)
+  python3 fix.py --canary 5                         # DRY-RUN first 5
+  ROLLFIX_CONFIRM=YES python3 fix.py --apply --canary 5   # LIVE canary (Steve-gated)
+"""
+import os, sys, json, time, urllib.request, urllib.error
+HERE = os.path.dirname(os.path.abspath(__file__))
+SHOP = "designer-laboratory-sandbox.myshopify.com"; API = "2024-10"
+
+def token():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip()
+    sys.exit("no SHOPIFY_ADMIN_TOKEN")
+
+def main():
+    apply = "--apply" in sys.argv
+    live = apply and os.environ.get("ROLLFIX_CONFIRM") == "YES"
+    if apply and not live:
+        print("REFUSING --apply without ROLLFIX_CONFIRM=YES. DRY-RUN instead.\n")
+    canary = int(sys.argv[sys.argv.index("--canary") + 1]) if "--canary" in sys.argv else None
+    work = json.load(open(os.path.join(HERE, "data", "worklist.json")))
+    if canary: work = work[:canary]
+    print(f"=== roll-on-fabric fix [{'LIVE' if live else 'DRY-RUN'}] — delete legacy "
+          f"global.unit_of_measure on {len(work)} Mulberry fabrics (reversible) ===\n")
+    tok = token() if live else None
+    done = err = 0
+    for i, w in enumerate(work, 1):
+        line = f"[{i}/{len(work)}] product {w['num']} mf_id={w['mf_id']} (was '{w['legacy_value']}', canon={w['canon']})"
+        if not live:
+            print("WOULD DELETE:", line); continue
+        try:
+            req = urllib.request.Request(f"https://{SHOP}/admin/api/{API}/metafields/{w['mf_id']}.json",
+                method="DELETE", headers={"X-Shopify-Access-Token": tok})
+            urllib.request.urlopen(req, timeout=40); done += 1; print("DELETED:", line)
+        except urllib.error.HTTPError as e:
+            err += 1; print(f"FAIL {line} -> HTTP {e.code}")
+        time.sleep(0.4)
+    print(f"\n{'deleted=%d failed=%d. Reverse with reverse.py' % (done, err) if live else 'DRY-RUN complete. Nothing written.'}")
+
+if __name__ == "__main__":
+    main()
diff --git a/reverse.py b/reverse.py
new file mode 100644
index 0000000..ac537d9
--- /dev/null
+++ b/reverse.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+"""Reverse: re-create the legacy global.unit_of_measure metafield on each product from
+data/worklist.json (new id, same namespace/key/value/type). DRY-RUN default; live needs
+--apply + ROLLFIX_CONFIRM=YES."""
+import os, sys, json, time, urllib.request, urllib.error
+HERE = os.path.dirname(os.path.abspath(__file__)); SHOP="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+def token():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=",1)[1].strip()
+    sys.exit("no token")
+def main():
+    live = "--apply" in sys.argv and os.environ.get("ROLLFIX_CONFIRM")=="YES"
+    work = json.load(open(os.path.join(HERE,"data","worklist.json")))
+    print(f"=== reverse [{'LIVE' if live else 'DRY-RUN'}] — re-create legacy metafield on {len(work)} ===")
+    tok = token() if live else None
+    for w in work:
+        body={"metafield":{"namespace":"global","key":"unit_of_measure",
+              "value":w['legacy_value'],"type":w.get('legacy_type') or "single_line_text_field"}}
+        if not live: print(f"WOULD re-create global.unit_of_measure='{w['legacy_value']}' on {w['num']}"); continue
+        try:
+            req=urllib.request.Request(f"https://{SHOP}/admin/api/{API}/products/{w['num']}/metafields.json",
+                data=json.dumps(body).encode(),method="POST",
+                headers={"X-Shopify-Access-Token":tok,"Content-Type":"application/json"})
+            urllib.request.urlopen(req,timeout=40); print(f"OK re-created on {w['num']}")
+        except urllib.error.HTTPError as e: print(f"FAIL {w['num']} HTTP {e.code}")
+        time.sleep(0.4)
+if __name__=="__main__": main()

(oldest)  ·  back to Roll On Fabric Fix  ·  (newest)