[object Object]

← back to Dw Material Envelope Canary

dw-material-envelope-canary: recurrence monitor for metafield double-encoding

6e5b25e6d4e4c24c75f86e0bcc4128ed62d6fcca · 2026-07-07 09:53:44 -0700 · steve

Files touched

Diff

commit 6e5b25e6d4e4c24c75f86e0bcc4128ed62d6fcca
Author: steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 09:53:44 2026 -0700

    dw-material-envelope-canary: recurrence monitor for metafield double-encoding
---
 .gitignore       |  5 ++++
 check.py         | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 data/latest.json |  7 +++++
 3 files changed, 103 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5bc0a74
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+data/*.log
+data/*.err
+.DS_Store
diff --git a/check.py b/check.py
new file mode 100644
index 0000000..dd74184
--- /dev/null
+++ b/check.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""dw-material-envelope-canary — READ-ONLY recurrence monitor.
+
+Scans the live DW Shopify catalog for the double-encoded metafield bug where a
+value leaked its raw JSON wrapper, e.g. {"type": "single_line_text_field",
+"value": "Paper"} — instead of the plain value. Born 2026-07-07 after a Python
+importer double-encoded custom.material + dwc.contents on 571 products.
+
+Alerts (CNCP parking-lot card) ONLY on FAIL (>0 envelopes found). PASS is silent.
+Always writes data/latest.json so the canary meta-watchdog sees it ran.
+Touches nothing — pure GraphQL reads."""
+import os, json, sys, time, urllib.request, datetime
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+def load_env():
+    for p in [os.path.expanduser("~/Projects/Designer-Wallcoverings/.env"),
+              os.path.join(HERE, ".env")]:
+        if os.path.exists(p):
+            for ln in open(p):
+                if "=" in ln and not ln.strip().startswith("#"):
+                    k, v = ln.strip().split("=", 1)
+                    os.environ.setdefault(k, v.strip().strip('"').strip("'"))
+load_env()
+DOMAIN = os.environ.get("SHOPIFY_STORE_DOMAIN", "designer-laboratory-sandbox.myshopify.com")
+TOK = os.environ.get("SHOPIFY_ADMIN_TOKEN") or os.environ.get("SHOPIFY_ADMIN_ACCESS_TOKEN")
+URL = f"https://{DOMAIN}/admin/api/2024-01/graphql.json"
+CNCP = "http://127.0.0.1:3333/api/parking_lot"
+
+# The known-corruptible keys. Add more here if the bug ever appears elsewhere.
+CHECK_KEYS = [("custom", "material"), ("dwc", "contents"), ("global", "Material")]
+FIELDS = " ".join(f'k{i}: metafield(namespace:"{ns}", key:"{k}"){{ value }}'
+                  for i, (ns, k) in enumerate(CHECK_KEYS))
+QUERY = "query($c:String){ products(first:200, after:$c){ pageInfo{ hasNextPage endCursor } edges{ node{ handle status " + FIELDS + " } } } }"
+
+def gql(cursor):
+    body = json.dumps({"query": QUERY, "variables": {"c": cursor}}).encode()
+    req = urllib.request.Request(URL, data=body, headers={
+        "X-Shopify-Access-Token": TOK, "Content-Type": "application/json"})
+    for a in range(6):
+        try:
+            with urllib.request.urlopen(req, timeout=60) as r:
+                d = json.loads(r.read())
+            if d.get("errors") and any("throttl" in str(e).lower() for e in d["errors"]):
+                time.sleep(2 + a); continue
+            return d
+        except Exception:
+            time.sleep(2 + a)
+    raise SystemExit("gql failed")
+
+def is_env(v):
+    v = (v or "").strip()
+    return v.startswith("{") and "single_line_text_field" in v
+
+def main():
+    total = 0; bad = []
+    cursor = None
+    while True:
+        conn = gql(cursor)["data"]["products"]
+        for e in conn["edges"]:
+            n = e["node"]; total += 1
+            for i in range(len(CHECK_KEYS)):
+                mf = n.get(f"k{i}")
+                if mf and is_env(mf.get("value")):
+                    ns, k = CHECK_KEYS[i]
+                    bad.append({"handle": n["handle"], "status": n["status"], "field": f"{ns}.{k}"})
+                    break
+        if not conn["pageInfo"]["hasNextPage"]:
+            break
+        cursor = conn["pageInfo"]["endCursor"]
+    verdict = "FAIL" if bad else "PASS"
+    result = {"ts": datetime.datetime.now().isoformat(), "scanned": total,
+              "broken": len(bad), "verdict": verdict, "sample": bad[:20]}
+    json.dump(result, open(os.path.join(HERE, "data", "latest.json"), "w"), indent=1)
+    print(f"{verdict}: scanned {total}, {len(bad)} envelope-corrupted")
+    if bad:
+        try:
+            active = sum(1 for b in bad if b["status"] == "ACTIVE")
+            card = {"title": f"⚠️ Material envelope regression: {len(bad)} products ({active} ACTIVE)",
+                    "note": f"dw-material-envelope-canary found metafield values leaking raw JSON wrappers again. "
+                            f"e.g. {', '.join(b['handle'] for b in bad[:5])}. Re-run /tmp/fix_material.py or check the importer.",
+                    "source": "dw-material-envelope-canary"}
+            urllib.request.urlopen(urllib.request.Request(
+                CNCP, data=json.dumps(card).encode(),
+                headers={"Content-Type": "application/json"}), timeout=5)
+            print("  posted CNCP alert")
+        except Exception as ex:
+            print("  CNCP post failed:", ex)
+    return 1 if bad else 0
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/data/latest.json b/data/latest.json
new file mode 100644
index 0000000..48ded35
--- /dev/null
+++ b/data/latest.json
@@ -0,0 +1,7 @@
+{
+ "ts": "2026-07-07T09:52:54.452209",
+ "scanned": 135398,
+ "broken": 0,
+ "verdict": "PASS",
+ "sample": []
+}
\ No newline at end of file

(oldest)  ·  back to Dw Material Envelope Canary  ·  canary: robust envelope detector + weekly all-metafields bul f11d1f0 →