← back to Flock Fix Viewer
flock: reversible combine roll+sample fix script (TK-11122, session close)
0f610a9566e3835ed8f11fc1ce7bac0e6da629db · 2026-09-02 15:33:23 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbJqkkF59hjUzX8PH7PHDN
Files touched
Diff
commit 0f610a9566e3835ed8f11fc1ce7bac0e6da629db
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 15:33:23 2026 -0700
flock: reversible combine roll+sample fix script (TK-11122, session close)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbJqkkF59hjUzX8PH7PHDN
---
apply-flock-combine.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 128 insertions(+)
diff --git a/apply-flock-combine.py b/apply-flock-combine.py
new file mode 100644
index 0000000..5823612
--- /dev/null
+++ b/apply-flock-combine.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""
+TK-11122 — flock split-brain fix. Rebuild the ACTIVE (thin sample) survivor into
+ONE combined roll+sample wallcovering SKU by copying the roll variant + metafields
+forward from its ARCHIVED rich twin. Archived twin is LEFT ARCHIVED (Steve: "if it
+was archived, leave it. We need 1 sku with roll and sample").
+
+Usage: python3 apply-flock-combine.py 1046 [--apply]
+ (no --apply = dry run: prints exactly what it will change, writes nothing)
+
+Reversible: survivor pre-state snapshot saved; ledgered.
+"""
+import os,sys,json,time,urllib.request,urllib.error
+
+# colorway -> (archived rich roll product id, active survivor product id)
+MAP={
+ "1045":(6785296007219,7862958030899),
+ "1046":(6785296662579,7862955311155),
+ "1047":(6785296990259,7862955868211),
+}
+LOC=5795643504 # 15442 Ventura Blvd (primary)
+TMPL=6785295810611 # 1044 combined = channel template
+
+cw=(sys.argv[1] if len(sys.argv)>1 else "").strip()
+APPLY="--apply" in sys.argv
+if cw not in MAP:
+ print("usage: apply-flock-combine.py <1045|1046|1047> [--apply]"); sys.exit(1)
+SRC,DST=MAP[cw]
+
+env=open(os.path.expanduser("~/Projects/secrets-manager/.env")).read()
+TOKEN=next((l.split("=",1)[1].strip().strip('"\'') for l in env.splitlines()
+ if l.startswith("SHOPIFY_FULL_ACCESS_TOKEN=")),None)
+STORE="designer-laboratory-sandbox.myshopify.com"
+API=f"https://{STORE}/admin/api/2024-10"
+def call(method,path,body=None):
+ data=json.dumps(body).encode() if body is not None else None
+ req=urllib.request.Request(f"{API}/{path}",data=data,method=method,
+ headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
+ try: return json.load(urllib.request.urlopen(req))
+ except urllib.error.HTTPError as e:
+ print(" HTTP",e.code,e.read().decode()[:400]); raise
+def gql(q,v=None):
+ req=urllib.request.Request(f"{API}/graphql.json",data=json.dumps({"query":q,"variables":v or {}}).encode(),
+ headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
+ return json.load(urllib.request.urlopen(req))
+
+src=call("GET",f"products/{SRC}.json")['product']
+dst=call("GET",f"products/{DST}.json")['product']
+roll_v=[v for v in src['variants'] if (v.get('sku') or '').upper()==f"FLOCK-{cw}"][0]
+samp_v=[v for v in dst['variants'] if 'sample' in (v.get('sku') or '').lower()][0]
+roll_opt=roll_v.get('option1') or 'Sold per single roll (20.5" x 5.5 yards)'
+roll_price=roll_v.get('price') or "190.99"
+
+tags=list(dict.fromkeys([t.strip() for t in src['tags'].split(',') if t.strip()]))
+tags=[t for t in tags if t.lower() not in ('display-exclude','memo sample')]
+if 'Phillipe Romano' not in tags: tags.append('Phillipe Romano')
+
+print(f"=== flock {cw} SRC(archived rich)={SRC} DST(active survivor)={DST} APPLY={APPLY}")
+print(f" will set DST title -> 'Sadie's Retro Block Flocked Velvet Wallcovering'")
+print(f" handle -> sadies-retro-block-flocked-velvet-{cw}")
+print(f" variants -> [ROLL {roll_v['sku']} ${roll_price} continue pos1] + [SAMPLE {samp_v['sku']} $4.25 deny pos2]")
+print(f" body_html <- archived roll ({len(src.get('body_html') or '')} chars); tags({len(tags)}) drop display-exclude/Memo Sample")
+print(f" copy metafields <- archived roll (skip custom.has_sample); delete stale custom.sample_of; publish to 1044 channels")
+if not APPLY:
+ print("\nDRY RUN — nothing written. Re-run with --apply to execute."); sys.exit(0)
+
+os.makedirs(os.path.expanduser("~/Projects/flock-fix-viewer/sadie-snapshots"),exist_ok=True)
+open(os.path.expanduser(f"~/Projects/flock-fix-viewer/sadie-snapshots/{cw}-survivor-pre.json"),"w").write(json.dumps(dst,indent=2))
+
+# A. product update
+payload={"product":{"id":DST,
+ "title":"Sadie's Retro Block Flocked Velvet Wallcovering",
+ "handle":f"sadies-retro-block-flocked-velvet-{cw}",
+ "body_html":src.get('body_html') or dst.get('body_html'),
+ "tags":", ".join(tags),
+ "options":[{"name":"Size"}],
+ "variants":[
+ {"option1":roll_opt,"sku":f"FLOCK-{cw}","price":str(roll_price),
+ "inventory_policy":"continue","inventory_management":"shopify","weight":3,"weight_unit":"lb","position":1},
+ {"id":samp_v['id'],"option1":"Sample","sku":f"FLOCK-{cw}-sample","price":"4.25",
+ "inventory_policy":"deny","inventory_management":"shopify","position":2},
+ ]}}
+print("\n[A] PUT product update…")
+res=call("PUT",f"products/{DST}.json",payload)['product']
+vs={v['sku']:v for v in res['variants']}
+roll=vs[f"FLOCK-{cw}"]; samp=vs[f"FLOCK-{cw}-sample"]
+print(" variants:",[(v['sku'],v['option1'],v['price'],'pos'+str(v['position'])) for v in res['variants']])
+
+# B. inventory
+print("[B] inventory set…")
+for v,qty in [(roll,1000),(samp,2026)]:
+ call("POST","inventory_levels/set.json",{"location_id":LOC,"inventory_item_id":v['inventory_item_id'],"available":qty})
+ print(f" {v['sku']} -> {qty}")
+
+# C. metafields
+print("[C] copy metafields…")
+copied=0
+for m in call("GET",f"products/{SRC}/metafields.json")['metafields']:
+ if (m['namespace'],m['key'])==('custom','has_sample'): continue
+ try:
+ call("POST",f"products/{DST}/metafields.json",{"metafield":{"namespace":m['namespace'],"key":m['key'],"type":m['type'],"value":m['value']}}); copied+=1
+ except Exception: pass
+for m in call("GET",f"products/{DST}/metafields.json")['metafields']:
+ if (m['namespace'],m['key'])==('custom','sample_of'):
+ call("DELETE",f"products/{DST}/metafields/{m['id']}.json"); print(" deleted stale custom.sample_of")
+print(f" copied {copied} metafields")
+
+# D. publish
+print("[D] publish to channels…")
+tmpl=gql('{ product(id:"gid://shopify/Product/%d"){ resourcePublicationsV2(first:20){edges{node{publication{id name} isPublished}}}}}'%TMPL)
+pubs=[e['node']['publication'] for e in tmpl['data']['product']['resourcePublicationsV2']['edges'] if e['node']['isPublished']]
+r=gql('mutation($id:ID!,$in:[PublicationInput!]!){publishablePublish(id:$id,input:$in){userErrors{field message}}}',
+ {"id":f"gid://shopify/Product/{DST}","in":[{"publicationId":p['id']} for p in pubs]})
+print(" ->",[p['name'] for p in pubs],"| errors:",r.get('data',{}).get('publishablePublish',{}).get('userErrors',[]) or "none")
+
+# ledger
+open(os.path.expanduser("~/.claude/yolo-queue/executed-reversible/ledger.jsonl"),"a").write(json.dumps({
+ "ts":time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),"agent":"vp-dw-commerce","ticket":"TK-11122",
+ "action":f"flock {cw}: rebuilt survivor {DST} -> combined roll+sample wallcovering; archived twin {SRC} left archived",
+ "blast_radius":1,"undo_cmd":f"restore ~/Projects/flock-fix-viewer/sadie-snapshots/{cw}-survivor-pre.json; delete roll variant {roll['id']}",
+ "verify":f"GET products/{DST}.json"})+"\n")
+
+# verify
+fin=call("GET",f"products/{DST}.json?fields=id,title,status,handle,variants")['product']
+print(f"\n[VERIFY] {fin['id']} {fin['status']} '{fin['title']}' /{fin['handle']}")
+for v in fin['variants']:
+ print(f" {v['sku']:20} {v['option1']:40} ${v['price']:>7} {v['inventory_policy']}")
+print(f"DONE flock {cw}.")
← 348c48a auto-data-snapshot: 2026-09-02T15:21:07 (1 data files) — sad
·
back to Flock Fix Viewer
·
auto-data-snapshot: 2026-09-02T16:03:42 (4 data files) — __p 65e3d3d →