← 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
A .gitignoreA check.pyA data/latest.json
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 →