← back to Imageless Rescue
scripts/remediate-active-violators.js
108 lines
#!/usr/bin/env node
'use strict';
/**
* SAFE remediation (DTD verdict A, 3/3, 2026-06-20) of the 2 confirmed LIVE
* ACTIVE activation-gate violators. ADDITIVE only — attaches the exact-match
* vendor image + width metafield + a DETERMINISTIC spec-driven description
* (NO metered LLM). Does NOT flip status, does NOT remove data, does NOT touch
* dw_unified. After this, validateBeforeActivate() passes for both → they stay
* ACTIVE and are gate-compliant.
*
* DRY-RUN by default; pass --commit to write to Shopify.
*/
const https = require('https');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { validateBeforeActivate } = require(path.join(os.homedir(),
'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
const COMMIT = process.argv.includes('--commit');
const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
function gql(q, v) {
return new Promise(res => {
const d = JSON.stringify({ query: q, variables: v || {} });
const r = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) } },
x => { let b=''; x.on('data',c=>b+=c); x.on('end',()=>{ try{res(JSON.parse(b))}catch{res({_raw:b.slice(0,300)})} }); });
r.on('error', e => res({ _err: e.message })); r.write(d); r.end();
});
}
// Deterministic spec-driven description (same shape as cadence-import.js buildInput).
function buildDesc(pattern, color, vendor, soldBy, specs) {
const li = [];
if (specs.material) li.push(`<li><strong>Material:</strong> ${esc(specs.material)}</li>`);
if (specs.width) li.push(`<li><strong>Width:</strong> ${esc(specs.width)}</li>`);
if (specs.length) li.push(`<li><strong>Roll Length:</strong> ${esc(specs.length)}</li>`);
if (specs.repeat) li.push(`<li><strong>Repeat:</strong> ${esc(specs.repeat)}</li>`);
li.push(`<li><strong>Sold By:</strong> ${esc(soldBy)}</li>`);
const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering by ${vendor}. Priced per ${soldBy.toLowerCase()}.`;
return `<p>${esc(intro)}</p><ul>${li.join('')}</ul>`;
}
// The 2 violators with their EXACT-match vendor data (verified against dw_unified).
const TARGETS = [
{
id: 'gid://shopify/Product/6604512165939',
vendor: 'China Seas', pattern: 'Petite Zig Zag', color: 'White on White',
soldBy: 'Single Roll',
image: 'https://quadrillefabrics.s3.us-east-2.amazonaws.com/Wallpaper_Images/Petite-Zig-Zag-White-on-White-Vinyl-AP303-01PV.jpg',
specs: { width: '27 in (68.58 cm)', material: '', length: '', repeat: '6 in (15.24 cm)' },
catalog: 'china_seas_catalog DWCH-510687 / mfr AP303-01PV',
},
{
id: 'gid://shopify/Product/7863634526259',
vendor: 'Tres Tintas', pattern: 'Pipelines Brick', color: 'Burnt Orange',
soldBy: 'Single Roll',
image: 'https://trestintas.com/wp-content/uploads/2025/12/pipelines-brick-repeated-patterns-coordonne.jpg',
specs: { width: '25.6 in (65 cm)', material: 'Bamboo', length: '', repeat: '' },
catalog: 'tres_tintas_catalog DWTS-901473 / mfr UT5401-5',
},
];
const M_UPDATE = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
const M_FILES = `mutation($id:ID!,$media:[CreateMediaInput!]!){productCreateMedia(productId:$id,media:$media){media{... on MediaImage{id status}} mediaUserErrors{field message code}}}`;
(async () => {
if (!TOKEN) { console.error('no token'); process.exit(1); }
for (const t of TARGETS) {
const desc = buildDesc(t.pattern, t.color, t.vendor, t.soldBy, t.specs);
const metafields = [
{ namespace:'global', key:'width', type:'single_line_text_field', value:t.specs.width },
{ namespace:'custom', key:'width', type:'single_line_text_field', value:t.specs.width },
];
if (t.specs.material) {
metafields.push({ namespace:'global', key:'material', type:'single_line_text_field', value:t.specs.material });
}
if (t.specs.repeat) metafields.push({ namespace:'global', key:'repeat', type:'single_line_text_field', value:t.specs.repeat });
metafields.push({ namespace:'global', key:'unit_of_measure', type:'single_line_text_field', value:`Priced Per ${t.soldBy}` });
console.log(`\n=== ${t.vendor} ${t.pattern} ${t.color} (${t.catalog}) ===`);
console.log(` image: ${t.image}`);
console.log(` width: ${t.specs.width} desc-len: ${desc.replace(/<[^>]+>/g,' ').trim().length}`);
if (!COMMIT) { console.log(' DRY-RUN — would attach image + width/material/repeat/uom metafields + description'); continue; }
// 1) description + metafields (additive)
const up = await gql(M_UPDATE, { input: { id: t.id, descriptionHtml: desc, metafields } });
const ue = up.data?.productUpdate?.userErrors || [];
if (ue.length) { console.log(` ✗ update error: ${JSON.stringify(ue).slice(0,160)}`); continue; }
console.log(' ✓ description + spec metafields attached');
await sleep(800);
// 2) image (Shopify fetches the external URL async)
const im = await gql(M_FILES, { id: t.id, media: [{ originalSource: t.image, mediaContentType:'IMAGE', alt: `${t.pattern} ${t.color} | ${t.vendor}` }] });
const ie = im.data?.productCreateMedia?.mediaUserErrors || [];
if (ie.length) { console.log(` ✗ image error: ${JSON.stringify(ie).slice(0,160)}`); continue; }
console.log(' ✓ exact-match vendor image attached (async fetch)');
await sleep(1200);
}
console.log(COMMIT ? '\nDONE (live). Re-run audit/verify to confirm gate-compliance.' : '\nDRY-RUN complete. Re-run with --commit.');
})();