← back to Dw Yolo Loop
Kravet dedup verifier: handle-driven keeper selection + redirect targets; 18-product verified archive plan (15 HIGH, 3 REVIEW)
cf0c7f0f281e0736e70dc6abff36be6c8b4e3dbe · 2026-06-15 10:52:20 -0700 · Steve Abrams
Files touched
A scripts/kravet-dedup-verify.js
Diff
commit cf0c7f0f281e0736e70dc6abff36be6c8b4e3dbe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 15 10:52:20 2026 -0700
Kravet dedup verifier: handle-driven keeper selection + redirect targets; 18-product verified archive plan (15 HIGH, 3 REVIEW)
---
scripts/kravet-dedup-verify.js | 62 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/scripts/kravet-dedup-verify.js b/scripts/kravet-dedup-verify.js
new file mode 100644
index 0000000..9edf6b2
--- /dev/null
+++ b/scripts/kravet-dedup-verify.js
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+/* READ-ONLY. For each dup cluster in /tmp/kravet_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/kravet_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/kravet_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/kravet_archive_plan.csv',out.join('\n'));
+ fs.writeFileSync('/tmp/kravet_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/kravet_archive_plan.csv | archive list (+redirects) -> /tmp/kravet_archive_list.csv`);
+})().catch(e=>{console.error(e);process.exit(1);});
← 7f7c9f1 Trending 2026 tag reconciler: newest-500 active + all Artmur
·
back to Dw Yolo Loop
·
Trending 2026: hard cap, raise to newest 1000 SKUs (no Artmu 5cb00ac →