← back to Tk10630 Sku Suffix Canary

classify-unrecoverable.mjs

51 lines

// READ-ONLY: classify the 2,538 unrecoverable products by best-guess recoverability.
// Rule: real code = first non-DW code from (1) variant base (ANY suffix),
// (2) manufacturer_sku metafield, (3) product_number. Else orphan.
import { gql } from './shopify.mjs';
import { FABRICATED } from './lib.mjs';
import { readFileSync, writeFileSync } from 'node:fs';

const { skipped } = JSON.parse(readFileSync('plan.json', 'utf8'));
const ids = skipped.map(s => s.id);
const isCode = s => /^[A-Z]{2,6}-?\d/i.test((s || '').trim()) && !/\s\w+\s\w+/.test(s || ''); // code-ish, not a long name
const cleanBase = s => { const m = (s || '').match(/^([A-Z]{2,6})-?(\d+)/i); return m ? `${m[1].toUpperCase()}-${m[2]}` : null; };
const nonDW = b => b && !FABRICATED.test(b);

const sleep = ms => new Promise(r => setTimeout(r, ms));
const tally = { from_variant: 0, from_mfr: 0, from_pn: 0, orphan: 0 };
const proposals = [], orphans = [];

for (let i = 0; i < ids.length; i += 50) {
  const chunk = ids.slice(i, i + 50).map(x => `"${x}"`).join(',');
  let data;
  for (let a = 0; ; a++) {
    try { ({ data } = await gql(`query{ nodes(ids:[${chunk}]){ ... on Product{ id handle status title
      mfr:metafield(namespace:"custom",key:"manufacturer_sku"){value}
      dmfr:metafield(namespace:"dwc",key:"manufacturer_sku"){value}
      pn:metafield(namespace:"global",key:"product_number"){value}
      variants(first:6){ nodes{ sku } } } } }`)); break; }
    catch (e) { if (/THROTTLED/i.test(e.message) && a < 8) { await sleep(2000 * (a + 1)); continue; } throw e; }
  }
  for (const p of data.nodes) {
    if (!p) continue;
    let code = null, src = null;
    // 1) non-DW base from any variant sku
    for (const v of p.variants.nodes) { const b = cleanBase(v.sku); if (nonDW(b)) { code = b; src = 'variant'; break; } }
    // 2) manufacturer_sku metafield
    if (!code) { const m = p.mfr?.value || p.dmfr?.value; if (isCode(m)) { const b = cleanBase(m); if (nonDW(b)) { code = b; src = 'mfr'; } } }
    // 3) product_number
    if (!code) { const b = cleanBase(p.pn?.value); if (nonDW(b)) { code = b; src = 'pn'; } }
    if (code) { tally['from_' + src]++; proposals.push({ id: p.id, handle: p.handle, status: p.status, code, src, mfrRaw: p.mfr?.value || p.dmfr?.value || null }); }
    else { tally.orphan++; orphans.push({ id: p.id, handle: p.handle, status: p.status, title: p.title, mfrRaw: p.mfr?.value || p.dmfr?.value || null, pn: p.pn?.value || null }); }
  }
  if (i % 500 === 0) process.stderr.write(`  ...${i}/${ids.length}\n`);
}
writeFileSync('recover-proposals.json', JSON.stringify(proposals, null, 0));
writeFileSync('recover-orphans.json', JSON.stringify(orphans, null, 0));
const recovered = proposals.length;
console.log(JSON.stringify({ total: ids.length, recovered, orphan: tally.orphan, by_source: tally,
  recovered_active: proposals.filter(p => p.status === 'ACTIVE').length,
  orphan_active: orphans.filter(o => o.status === 'ACTIVE').length }, null, 2));
console.log('sample proposals:', proposals.slice(0, 8).map(p => `${p.handle}->${p.code}(${p.src})`).join('  '));
console.log('sample orphans:', orphans.slice(0, 6).map(o => `${o.handle}[mfr=${o.mfrRaw}]`).join('  '));