← 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(' '));