← back to Flock Fix Viewer
enrich-rollout.mjs
84 lines
// Flock spec enrichment rollout: match each DW flock product to its Astek VV pattern,
// write verified specs (repeat per-pattern; match/print/backing/application/removal/material uniform)
// to BOTH global + custom namespaces. Reversible + ledgered. Modes: --dry | --apply
import fs from 'fs'; import os from 'os'; import path from 'path';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const TOKEN=fs.readFileSync(new URL('./.token',import.meta.url),'utf8').trim();
const LEDGER=path.join(os.homedir(),'.claude/yolo-queue/executed-reversible/ledger.jsonl');
const DRY=!process.argv.includes('--apply');
const API=`https://${SHOP}/admin/api/2024-10`;
const H={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const gql=async(q,v={})=>(await (await fetch(`${API}/graphql.json`,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})})).json());
// pattern key from a title/name: strip DW first-name, colorway, boilerplate
function patKey(t){return (t||'').toLowerCase()
.replace(/^vv\d+\s*-\s*/,'').replace(/\s*\|.*$/,'')
.replace(/memo sample|flocked? velvet|flocked? wallcovering|wallcovering|wallpaper|\bmemo\b|\bsample\b|damask/g,'')
.replace(/^\w+['’]s\s+/,'') // drop leading "Florence's "
.replace(/[^a-z0-9 ]/g,' ').replace(/\s+/g,' ').trim();}
// Verified Astek VV-Flocked (Vogue Velour) per-pattern vertical repeat. EXACT longest-substring match.
const ASTEK={
'st. moritz':'27.25 Inches','st moritz':'27.25 Inches',
'madison':'36 Inches','empire':'43.5 Inches','griffons':'35 Inches',
'zodiac':'32.5 Inches','art deco fans':'24 Inches','art deco':'24 Inches',
'lattice':'39 Inches','grille':'25.5 Inches','medallions':'39 Inches',
};
const ASTEK_KEYS=Object.keys(ASTEK).sort((a,b)=>b.length-a.length);
function match1(s){ const t=(s||'').toLowerCase(); for(const k of ASTEK_KEYS){ if(t.includes(k)) return k; } return null; }
// Match on handle AND title; require agreement. Disagreement = mislabeled dup -> skip (don't guess).
function matchRepeat(handle,title){
const h=match1(handle), t=match1(title);
if(h && t && ASTEK[h]!==ASTEK[t]) return {conflict:true, h, t}; // e.g. handle=madison but title=empire
const k=h||t; if(!k) return null;
return {repeat:ASTEK[k], name:k};
}
const UNIFORM=[
['Match','match_type','Straight'],
['application','application','Paste the Paper'],
['Substrate','backing','Paper'],
['removability','removal','Strippable'],
['print_type','print_type','Flock'],
];
const UOM='Priced Per Single Roll. Ships untrimmed (30" untrimmed / 27" trimmed width).';
(async()=>{
console.log('Astek patterns:',ASTEK_KEYS.length);
// DW flock priced products
let cur=null,all=[];
const Q=`query($c:String){ products(first:100, query:"(tag:'Flock Velvet' OR title:flock OR handle:flock)", after:$c){ pageInfo{hasNextPage endCursor} edges{node{ id handle title status variants(first:6){edges{node{title price}}} }}}}`;
do{const r=await gql(Q,{c:cur});const p=r?.data?.products;if(!p)break;all=all.concat(p.edges);cur=p.pageInfo.hasNextPage?p.pageInfo.endCursor:null;}while(cur);
const priced=all.map(e=>e.node).filter(n=>n.status!=='ARCHIVED' && n.variants.edges.map(e=>e.node).some(v=>!/sample/i.test(v.title||'')&&parseFloat(v.price)>10));
let matched=0,unmatched=[],conflicts=[],done=0;
for(const n of priced){
const hit=matchRepeat(n.handle,n.title);
if(!hit){unmatched.push(n.handle);continue;}
if(hit.conflict){conflicts.push(n.handle+' (handle='+hit.h+' vs title='+hit.t+')');continue;}
matched++;
const mf=[];
// per-pattern repeat (global.repeat + custom.pattern_repeat)
mf.push({ownerId:n.id,namespace:'global',key:'repeat',type:'single_line_text_field',value:hit.repeat});
mf.push({ownerId:n.id,namespace:'custom',key:'pattern_repeat',type:'single_line_text_field',value:hit.repeat});
// uniform specs to global + custom
for(const [gk,ck,val] of UNIFORM){
mf.push({ownerId:n.id,namespace:'global',key:gk,type:'single_line_text_field',value:val});
mf.push({ownerId:n.id,namespace:'custom',key:ck,type:'single_line_text_field',value:val});
}
// fix uom in both namespaces
mf.push({ownerId:n.id,namespace:'global',key:'unit_of_measure',type:'single_line_text_field',value:UOM});
mf.push({ownerId:n.id,namespace:'custom',key:'unit_of_measure',type:'single_line_text_field',value:UOM});
if(DRY){ if(matched<=8) console.log(` ${n.handle.slice(0,42)} -> repeat ${hit.repeat} (${hit.name})`); continue; }
const r=await gql('mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}',{mf});
const errs=r?.data?.metafieldsSet?.userErrors;
if(errs&&errs.length){console.log(' FAIL',n.handle,JSON.stringify(errs));continue;}
done++;
fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),agent:'main:flock-enrich-rollout',ticket:'flock-enrich',action:`enrich ${n.handle} (repeat ${hit.repeat} + uniform flock specs, global+custom)`,blast_radius:1,undo_cmd:`delete added spec metafields on ${n.id}`,verify:`spec table shows repeat/match/removal`})+'\n');
if(done%10===0)console.log(` …${done}/${matched}`);
await sleep(220);
}
console.log(`${DRY?'DRY':'DONE'} · matched ${matched} · applied ${done} · unmatched ${unmatched.length} · conflicts ${conflicts.length}`);
if(conflicts.length) console.log(' CONFLICTS (handle/title disagree — skipped, need manual):', conflicts.join(' | '));
})();