← back to Dw Yolo Loop
scripts/dedup-verify-all.js
63 lines
#!/usr/bin/env node
/* READ-ONLY. For each dup cluster in /tmp/fleet_dedup.csv, fetch both copies' keeper signals
(Online Store publication, inventory, image count, price, handle) and recommend KEEP vs ARCHIVE.
Keeper priority: (1) published to Online Store, (2) has price, (3) has images, (4) inventory>0,
(5) cleaner handle (no trailing -N), (6) newest. Flags CONFLICT when BOTH are live (human decides).
Writes /tmp/fleet_archive_plan.csv. NO writes/archives. */
const https=require('https'), fs=require('fs');
const T=process.env.T, S='designer-laboratory-sandbox.myshopify.com';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gqlOnce(q){return new Promise((res,rej)=>{const rq=https.request(`https://${S}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>{try{res(JSON.parse(b))}catch(e){rej(b.slice(0,200))}})});rq.on('error',rej);rq.setTimeout(30000,()=>rq.destroy(new Error('timeout')));rq.write(JSON.stringify({query:q}));rq.end();});}
async function gql(q){for(let i=0;i<4;i++){try{return await gqlOnce(q);}catch(e){if(i===3)throw e;await sleep(1200*(i+1));}}}
const csv=fs.readFileSync('/tmp/fleet_dedup.csv','utf8').trim().split('\n').slice(1);
const clusters={};
for(const line of csv){ const m=line.match(/^("(?:[^"]|"")*"|[^,]*),([^,]*),([^,]*),([^,]*),([^,]*),/); if(!m)continue;
const key=m[1].replace(/^"|"$/g,''), vendor=m[2], sku=m[3], pid=m[4];
(clusters[key]=clusters[key]||{vendor,sku,ids:[]}).ids.push(pid); }
async function signals(pid){
const q=`{ product(id:"gid://shopify/Product/${pid}"){ handle status totalInventory createdAt
mediaCount{ count }
variants(first:10){ edges{ node{ price selectedOptions{ value } } } }
resourcePublications(first:25, onlyPublished:true){ edges{ node{ publication{ name } } } } } }`;
const s=await gql(q); const p=s.data?.product; if(!p) return null;
const pubs=(p.resourcePublications?.edges||[]).map(e=>e.node.publication?.name).filter(Boolean);
const reals=(p.variants?.edges||[]).map(e=>({pr:parseFloat(e.node.price),sm:(e.node.selectedOptions||[]).some(o=>/sample/i.test(o.value))})).filter(v=>!v.sm&&v.pr>5);
return { pid, handle:p.handle, status:p.status, inv:p.totalInventory??0, created:p.createdAt,
media:p.mediaCount?.count||0, price:reals.length?Math.max(...reals.map(v=>v.pr)):0,
onStore:pubs.some(n=>/online store/i.test(n)), pubCount:pubs.length };
}
const dupSuffix=h=>/-\d+$/.test(h||''); // shopify duplicate-handle marker (-1, -2)
const codedHandle=h=>/^dwkk-/i.test(h||''); // old auto-generated code handle
// higher = better keeper
const handleRank=h=> (dupSuffix(h)?0:1) + (codedHandle(h)?0:2); // named clean slug = 3, coded = 1, -N dup = 0/2
(async()=>{
const out=['cluster,vendor,sku,decision,archive_id,archive_handle,keep_id,keep_handle,keep_price,keep_img,confidence'];
const archiveList=['archive_product_id,redirect_from_handle,redirect_to_handle,vendor,sku'];
let archive=0, review=0;
for(const [key,c] of Object.entries(clusters)){
const sig=[]; for(const pid of c.ids){ const x=await signals(pid); if(x) sig.push(x); await sleep(200); }
if(sig.length<2){ out.push(`"${key}",${c.vendor},${c.sku},NEED-EYES,,,,,,,fetch-incomplete`); continue; }
// keeper score: handle quality dominates, then real price, images, inventory, recency
const score=x=> handleRank(x.handle)*1000 + (x.price>5?300:0) + Math.min(x.media,9)*20 + (x.inv>0?10:0) + (new Date(x.created).getTime()/1e14);
sig.forEach(x=>x._s=score(x));
const ranked=sig.slice().sort((a,b)=>b._s-a._s);
const keep=ranked[0];
// confidence: HIGH if a clear handle signal separates them, else REVIEW (both coded, ~equal)
const handlesDiffer = new Set(sig.map(x=>handleRank(x.handle))).size>1 || sig.some(x=>x.price>5)&&!sig.every(x=>x.price>5);
const conf = handlesDiffer ? 'HIGH' : 'REVIEW';
if(conf==='REVIEW') review++;
for(const x of ranked.slice(1)){ archive++;
out.push(`"${key}",${c.vendor},${c.sku},ARCHIVE,${x.pid},${x.handle},${keep.pid},${keep.handle},${keep.price||''},${keep.media},${conf}`);
archiveList.push(`${x.pid},${x.handle},${keep.handle},${c.vendor},${c.sku}`);
}
}
fs.writeFileSync('/tmp/fleet_archive_plan.csv',out.join('\n'));
fs.writeFileSync('/tmp/fleet_archive_list.csv',archiveList.join('\n'));
console.log('=== KRAVET DEDUP — VERIFIED ARCHIVE PLAN (read-only) ===');
console.log(`clusters: ${Object.keys(clusters).length} | products to ARCHIVE: ${archive} | HIGH-confidence: ${archive-review} | REVIEW (both coded, ~equal): ${review}`);
console.log(`plan -> /tmp/fleet_archive_plan.csv | archive list (+redirects) -> /tmp/fleet_archive_list.csv`);
})().catch(e=>{console.error(e);process.exit(1);});