[object Object]

← back to Dw Rotation Activator

Add mdc-activate.js: private-label MDC→Phillipe Romano activator (mdc_catalog PID-scoped, settlement+5-field gate, ex-Google, MDC-coinage leak guard, Dublin-Vital hold)

04ef58e6fdb0967f110003929b54916ede77b2e3 · 2026-07-26 20:14:52 -0700 · Steve

Files touched

Diff

commit 04ef58e6fdb0967f110003929b54916ede77b2e3
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 20:14:52 2026 -0700

    Add mdc-activate.js: private-label MDC→Phillipe Romano activator (mdc_catalog PID-scoped, settlement+5-field gate, ex-Google, MDC-coinage leak guard, Dublin-Vital hold)
---
 mdc-activate.js | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/mdc-activate.js b/mdc-activate.js
new file mode 100644
index 0000000..1468b1c
--- /dev/null
+++ b/mdc-activate.js
@@ -0,0 +1,101 @@
+// MDC → Phillipe Romano (Type II) PRIVATE-LABEL settlement-gated activator.
+// Sibling of cmo-activate.js. Reuses the REAL SettlementGate (legal gate) +
+// validateBeforeActivate (5-field/specs gate). DRAFT→ACTIVE + publish ex-Google.
+//
+// PRIVATE-LABEL SCOPING: "Phillipe Romano" is shared by three place-name lines
+// (MDC=world capitals, Command54=East-Coast beaches, Greenland=coastal cities), so a
+// vendor:'Phillipe Romano' query would over-match. This activator is driven by the
+// EXACT shopify_product_id set recorded in dw_unified.mdc_catalog — the authoritative
+// MDC set, nothing else.
+//
+// HARD RAILS
+//  * Re-runs a distinctive private-label LEAK guard on the customer-facing surface
+//    (title/tags/body) right before ACTIVE — an MDC/book/designer coinage → HOLD,
+//    never activate (activation is the customer-facing moment).
+//  * The 48 "Dublin Vital" rows (generic word, but the MDC line name) are HELD for
+//    human review unless --allow-vital is passed.
+//  * Quote-only line: sample + `quotes` tag + ≥2 tags exemption; publish EX-Google
+//    (quote-only stays off Google Shopping).
+//  * DRY by default. --commit performs the live activation. --limit N samples.
+//
+// Cost: settlement gate may hit a paid vision tier on some images; tracked + printed.
+//
+// Usage:
+//   node mdc-activate.js --limit 20            # DRY sample (prove + estimate settlement $)
+//   node mdc-activate.js                       # DRY over the full MDC set
+//   node mdc-activate.js --commit              # LIVE activate (Steve-gated)
+const fs=require('fs'), os=require('os'), path=require('path');
+const { SettlementGate } = require('./lib/settlement-gate.js');
+const { validateBeforeActivate } = require(path.join(os.homedir(),'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
+const { execSync } = require('child_process');
+const TOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1];
+const STORE='designer-laboratory-sandbox.myshopify.com', API='2024-10';
+const GOOGLE='gid://shopify/Publication/29646651457';
+const DRY = !process.argv.includes('--commit');
+const ALLOW_VITAL = process.argv.includes('--allow-vital');
+const LIMIT = process.argv.includes('--limit') ? parseInt(process.argv[process.argv.indexOf('--limit')+1],10) : Infinity;
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const L=fs.createWriteStream('/tmp/mdc_activate.log',{flags:'w'});
+const P=m=>{L.write(m+'\n');console.log(m);};
+
+// distinctive private-label coinages that must NEVER go customer-facing (generic
+// words like "vital" are handled separately via the Dublin-Vital hold below).
+const HARD_LEAK=/\b(mdc|mdcwall|vycon|genon|bolta|lentex|nvolve|candice\s+olson|thom\s+filicia|jonathan\s+mark|taniya\s+nayak)\b/i;
+
+async function gql(q,v){ for(let a=0;a<4;a++){ try{ const r=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}); const j=await r.json(); if(j.errors&&a<3){await sleep(1500);continue;} return j;}catch(e){await sleep(1500);} } return {}; }
+const mfVal=(mfs,ns,k)=>{const x=mfs.find(m=>m.namespace===ns&&m.key===k);return x?x.value:'';};
+async function loadPubs(){ const r=await gql(`{publications(first:50){edges{node{id name}}}}`,{}); return (r.data?.publications?.edges||[]).map(e=>e.node).filter(p=>p.id!==GOOGLE); }
+async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mfs){userErrors{message}}}`,{mfs:[{ownerId:id,namespace:'internal',key:'settlement_hold',type:'single_line_text_field',value:`${verdict}:${reason}`.slice(0,250)}]}); }
+
+// ---- MDC set from staging (authoritative). PID -> {mfr,material,width,dw_sku,vital}
+const rows=execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT shopify_product_id,mfr_sku,coalesce(material,''),coalesce(width,''),dw_sku,(private_label_collection='Dublin' AND pattern_name ~* 'vital') FROM mdc_catalog WHERE shopify_product_id IS NOT NULL ORDER BY dw_sku;"`).toString().trim().split('\n');
+const SET=rows.map(l=>{const[pid,mfr,mat,width,dw,vital]=l.split('\t');return{pid,mfr,mat,width,dw,vital:vital==='t'};}).filter(r=>r.pid);
+
+(async()=>{
+  const gate=new SettlementGate(); P(`settlement lock: ${JSON.stringify(gate.lockStatus())}  mode=${DRY?'DRY':'COMMIT'}  mdc_set=${SET.length}  allowVital=${ALLOW_VITAL}`);
+  const pubs=DRY?[]:await loadPubs(); if(!DRY)P(`publications (ex-Google): ${pubs.map(p=>p.name).join(', ')}`);
+  let scanned=0,activated=0,publishedN=0,heldSettle=0,heldValidate=0,blocked=0,heldLeak=0,heldVital=0,cost=0;
+  const todo=SET.slice(0,LIMIT===Infinity?SET.length:LIMIT);
+  for(const row of todo){
+    scanned++;
+    const gid=`gid://shopify/Product/${row.pid}`;
+    const r=await gql(`query($id:ID!){product(id:$id){id title status tags descriptionHtml featuredImage{url} images(first:6){nodes{url}} variants(first:5){nodes{sku price}} metafields(first:40){nodes{namespace key value}}}}`,{id:gid});
+    const n=r.data?.product; if(!n){P(`load-miss ${row.dw} pid=${row.pid}`);continue;}
+    if(n.status!=='DRAFT'){P(`skip ${row.dw}: status=${n.status}`);continue;}
+    const mfs=n.metafields.nodes; const imgs=(n.images.nodes||[]).map(i=>i.url);
+    const width=mfVal(mfs,'global','width')||row.width;
+    // (0) private-label leak guard on customer-facing surface — HARD, never activate a leak
+    const surface=`${n.title} ${(n.tags||[]).join(' ')} ${n.descriptionHtml||''}`;
+    if(HARD_LEAK.test(surface)){ heldLeak++; P(`HOLD(leak) ${row.dw} ${row.mfr}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Hold-Leak-Review']}); continue; }
+    // (0b) Dublin "Vital" generic-word hold — needs human OK before customer-facing
+    if(row.vital && !ALLOW_VITAL){ heldVital++; P(`HOLD(vital-review) ${row.dw} ${n.title}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Hold-Review']}); continue; }
+    // (1) 5-field / specs gate (real validator)
+    const val=validateBeforeActivate({title:n.title,dwSku:row.dw,vendor:'Phillipe Romano',tags:n.tags||[],
+      descriptionHtml:n.descriptionHtml,variants:n.variants.nodes,
+      specs:{width,material:row.mat,repeat:'Free match'},vendorSpecs:{width,material:row.mat},
+      images:imgs,vendorImages:imgs});
+    const hasSample=(n.variants.nodes||[]).some(v=>/-sample$/i.test(v.sku||''));
+    const extraOk=hasSample && (n.tags||[]).length>=2;
+    if(!val.ok || !extraOk){ heldValidate++; const why=[...(val.reasons||[]),...(!hasSample?['no-sample']:[])].join(',');
+      P(`HOLD(validate) ${row.dw} ${row.mfr} :: ${why}`);
+      if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Needs-Description']}); continue; }
+    // (2) settlement (legal) gate
+    const sv=await gate.evaluate({shopify_id:n.id,title:n.title,vendor:'Phillipe Romano',dw_sku:row.dw,material:row.mat,imageUrl:n.featuredImage?.url||imgs[0]||''});
+    cost+=sv.cost||0;
+    if(sv.verdict!=='PASS'){ if(sv.verdict==='BLOCK')blocked++;else heldSettle++;
+      P(`HOLD(settlement:${sv.verdict}) ${row.dw} ${row.mfr} [${sv.tier}] :: ${sv.reason}`);
+      if(!DRY) await setHold(n.id,sv.verdict,sv.reason); continue; }
+    // PASS → activate + New Arrival + publish (ex-Google)
+    if(DRY){ activated++; continue; }
+    const ar=await gql(`mutation($id:ID!){productUpdate(input:{id:$id,status:ACTIVE}){userErrors{message}}}`,{id:n.id});
+    const ue=ar.data?.productUpdate?.userErrors||[]; if(ue.length){P(`ACTIVATE-ERR ${row.dw}: ${JSON.stringify(ue)}`);continue;}
+    activated++;
+    await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['New Arrival']});
+    const pp=await gql(`mutation($id:ID!,$in:[PublicationInput!]!){publishablePublish(id:$id,input:$in){userErrors{message}}}`,{id:n.id,in:pubs.map(p=>({publicationId:p.id}))});
+    const pe=(pp.data?.publishablePublish?.userErrors||[]).filter(e=>!/already/i.test(e.message||''));
+    if(!pe.length) publishedN++;
+    if(activated%25===0)P(`  ...activated ${activated}`);
+    await sleep(300);
+  }
+  P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} wouldActivate=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate} heldLeak=${heldLeak} heldVital=${heldVital}  settlement_cost=$${cost.toFixed(4)}`);
+})();

← f8d9655 auto-save: 2026-07-25T12:36:15 (1 files) — cmo-activate.js  ·  back to Dw Rotation Activator  ·  chore: refactor (summary label reflects COMMIT vs DRY mode), 6ec5f49 →