[object Object]

← back to Dw Yolo Loop

chore: remove dead rw-price-group one-off cluster (9 unreferenced Rebel Walls price-group experiment forks, ~2700 LOC)

fd37d081b7e38e7dc689990de1cc99244d81fb80 · 2026-06-15 23:00:31 -0700 · Steve Abrams

All 9 confirmed zero external references (no plist/shell/import). Reversible via git history. Council Eng#1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit fd37d081b7e38e7dc689990de1cc99244d81fb80
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 23:00:31 2026 -0700

    chore: remove dead rw-price-group one-off cluster (9 unreferenced Rebel Walls price-group experiment forks, ~2700 LOC)
    
    All 9 confirmed zero external references (no plist/shell/import). Reversible via git history. Council Eng#1.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 rw-price-group-680.js            | 320 ------------------------------
 rw-price-group-fix.js            | 302 ----------------------------
 rw-price-group-offset520.js      | 403 --------------------------------------
 rw-price-group-slice5.js         | 413 ---------------------------------------
 rw-price-group.js                | 363 ----------------------------------
 scripts/rw-price-group-fix.js    | 398 -------------------------------------
 scripts/rw-price-group-setter.js | 331 -------------------------------
 scripts/rw-price-group-slice7.js | 318 ------------------------------
 tmp-rw-price-group.js            | 337 --------------------------------
 9 files changed, 3185 deletions(-)

diff --git a/rw-price-group-680.js b/rw-price-group-680.js
deleted file mode 100644
index ebc4f8f..0000000
--- a/rw-price-group-680.js
+++ /dev/null
@@ -1,320 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-680.js
- * Determine Rebel Walls price group (C2/C3/C4) from live RW product page per-m2 rate.
- * Slice: LIMIT 40 OFFSET 680
- * Sandbox only.
- */
-
-const https = require('https');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7855827910707',
-  'gid://shopify/Product/7855827943475',
-  'gid://shopify/Product/7855827976243',
-  'gid://shopify/Product/7855828009011',
-  'gid://shopify/Product/7855828074547',
-  'gid://shopify/Product/7855828140083',
-  'gid://shopify/Product/7855828172851',
-  'gid://shopify/Product/7855828238387',
-  'gid://shopify/Product/7855828271155',
-  'gid://shopify/Product/7855828303923',
-  'gid://shopify/Product/7855828336691',
-  'gid://shopify/Product/7855828402227',
-  'gid://shopify/Product/7855828434995',
-  'gid://shopify/Product/7855828467763',
-  'gid://shopify/Product/7855828533299',
-  'gid://shopify/Product/7855828566067',
-  'gid://shopify/Product/7855828598835',
-  'gid://shopify/Product/7855828631603',
-  'gid://shopify/Product/7855828664371',
-  'gid://shopify/Product/7855828729907',
-  'gid://shopify/Product/7855828860979',
-  'gid://shopify/Product/7855829024819',
-  'gid://shopify/Product/7855829221427',
-  'gid://shopify/Product/7855829483571',
-  'gid://shopify/Product/7855829647411',
-  'gid://shopify/Product/7855829712947',
-  'gid://shopify/Product/7855829745715',
-  'gid://shopify/Product/7855829778483',
-  'gid://shopify/Product/7855829876787',
-  'gid://shopify/Product/7855829909555',
-  'gid://shopify/Product/7855829942323',
-  'gid://shopify/Product/7855829975091',
-  'gid://shopify/Product/7855830007859',
-  'gid://shopify/Product/7855830040627',
-  'gid://shopify/Product/7855829811251',
-  'gid://shopify/Product/7855830073395',
-  'gid://shopify/Product/7855830106163',
-  'gid://shopify/Product/7855830138931',
-  'gid://shopify/Product/7855830171699',
-  'gid://shopify/Product/7855830204467',
-];
-
-// Price group buckets (base/cheapest material per-m2)
-// C2 ~58.10, C3 ~76.40, C4 ~88.30
-const C2_MATERIALS = [
-  {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-  {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-  {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-];
-const C4_MATERIALS = [
-  {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-  {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-  {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-];
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const lowBudget = (result?.extensions?.cost?.throttleStatus?.currentlyAvailable || 9999) < 200;
-      if (throttled) { await sleep(3000); continue; }
-      if (lowBudget) await sleep(1500);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-function httpsGet(url, redirects = 5) {
-  return new Promise((resolve, reject) => {
-    const parsedUrl = new URL(url);
-    const options = {
-      hostname: parsedUrl.hostname,
-      path: parsedUrl.pathname + parsedUrl.search,
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.5',
-      }
-    };
-    const req = https.request(options, res => {
-      if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location && redirects > 0) {
-        const next = res.headers.location.startsWith('http') ? res.headers.location : `https://${parsedUrl.hostname}${res.headers.location}`;
-        res.resume();
-        return resolve(httpsGet(next, redirects - 1));
-      }
-      let body = '';
-      res.on('data', d => body += d);
-      res.on('end', () => resolve({ status: res.statusCode, body }));
-    });
-    req.on('error', reject);
-    req.setTimeout(20000, () => { req.destroy(); reject(new Error('fetch timeout')); });
-    req.end();
-  });
-}
-
-/**
- * Extract base per-m2 price from Rebel Walls product page HTML.
- * RW uses JSON-LD Product schema with offers.price in USD/m2.
- * Returns a number (USD/m2) or null.
- */
-function extractRWPrice(html) {
-  // Primary: JSON-LD Product schema — offers.price is base material price per m2
-  const jsonLdBlocks = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const block of jsonLdBlocks) {
-    const inner = block.replace(/<script[^>]*>/, '').replace(/<\/script>/, '');
-    try {
-      const obj = JSON.parse(inner);
-      if (obj['@type'] === 'Product' && obj.offers?.price) {
-        const p = parseFloat(obj.offers.price);
-        if (!isNaN(p) && p > 10) return p; // sanity: RW prices are 58-120 range
-      }
-    } catch {}
-  }
-
-  // Fallback: any "price": "XX.X" in JSON-LD in the valid range
-  const allPrices = [];
-  const priceRe = /"price"\s*:\s*"?([\d.,]+)"?/g;
-  let m;
-  while ((m = priceRe.exec(html)) !== null) {
-    const val = parseFloat(m[1].replace(',', ''));
-    if (!isNaN(val) && val >= 40 && val <= 200) allPrices.push(val);
-  }
-  if (allPrices.length > 0) {
-    // Return the minimum (base material tier)
-    return Math.min(...allPrices);
-  }
-
-  return null;
-}
-
-/**
- * Bucket a per-m2 price into C2/C3/C4
- * C2 ~58.10, C3 ~76.40, C4 ~88.30
- */
-function bucketPrice(pricePerM2) {
-  if (pricePerM2 === null || pricePerM2 === undefined) return null;
-  // Use midpoint thresholds
-  // C2 < 67.25 (midpoint of 58.10 and 76.40)
-  // C3 between 67.25 and 82.35 (midpoint of 76.40 and 88.30)
-  // C4 >= 82.35
-  if (pricePerM2 < 67.25) return 'C2';
-  if (pricePerM2 < 82.35) return 'C3';
-  return 'C4';
-}
-
-const MF_MUTATION = 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message field } } }';
-
-async function writeMetafields(pid, group, currentGroup) {
-  const mfs = [
-    { ownerId: pid, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-  ];
-
-  if (group !== 'C3') {
-    const costPerM2 = group === 'C2' ? '33.84' : '51.44';
-    const materials = group === 'C2' ? C2_MATERIALS : C4_MATERIALS;
-    const costBasis = `${group} portal-RRP-derived; Non-Woven retail $${group === 'C2' ? '58.10' : '88.30'}/m2 (x0.5825 net); P&S/Commercial extrapolated`;
-    mfs.push(
-      { ownerId: pid, namespace: 'custom', key: 'cost_per_m2', value: costPerM2, type: 'number_decimal' },
-      { ownerId: pid, namespace: 'custom', key: 'material_options', value: JSON.stringify(materials), type: 'json' },
-      { ownerId: pid, namespace: 'custom', key: 'cost_basis', value: costBasis, type: 'single_line_text_field' },
-    );
-  }
-
-  const r = await gql({ query: MF_MUTATION, variables: { m: mfs } });
-  const errs = r?.data?.metafieldsSet?.userErrors || [];
-  if (errs.length) {
-    throw new Error(`metafieldsSet errors: ${errs.map(e => e.message).join('; ')}`);
-  }
-  return mfs.length;
-}
-
-async function processProduct(pid) {
-  // 1. Fetch rw_product_url + price_group
-  const q = `{ product(id:"${pid}") {
-    title
-    metafields(first:50){edges{node{namespace key value}}}
-  }}`;
-  const fr = await gql({ query: q });
-  const p = fr?.data?.product;
-  if (!p) throw new Error(`product not found: ${pid}`);
-
-  const mfMap = {};
-  for (const e of (p.metafields?.edges || [])) {
-    const { namespace, key, value } = e.node;
-    if (namespace === 'custom') mfMap[key] = value;
-  }
-
-  const rwUrl = mfMap['rw_product_url'];
-  const currentGroup = mfMap['price_group'];
-
-  if (!rwUrl) {
-    return { status: 'skip', reason: 'no rw_product_url' };
-  }
-
-  // 2. Fetch RW product page
-  let html;
-  try {
-    const resp = await httpsGet(rwUrl);
-    if (resp.status !== 200) {
-      return { status: 'unknown', reason: `HTTP ${resp.status} from ${rwUrl}` };
-    }
-    html = resp.body;
-  } catch (e) {
-    return { status: 'unknown', reason: `fetch error: ${e.message}` };
-  }
-
-  // 3. Extract price
-  const priceM2 = extractRWPrice(html);
-  if (priceM2 === null) {
-    return { status: 'unknown', reason: `could not extract price from ${rwUrl}`, html_snippet: html.slice(0, 500) };
-  }
-
-  // 4. Bucket
-  const group = bucketPrice(priceM2);
-
-  // 5. Write metafields
-  await writeMetafields(pid, group, currentGroup);
-  await sleep(500);
-
-  return { status: 'ok', group, priceM2, previousGroup: currentGroup, title: p.title };
-}
-
-async function main() {
-  const counts = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0, skip: 0 };
-  const notes = [];
-  const results = [];
-
-  for (let i = 0; i < PRODUCT_IDS.length; i++) {
-    const pid = PRODUCT_IDS[i];
-    const short = pid.replace('gid://shopify/Product/', '');
-    try {
-      const res = await processProduct(pid);
-      results.push({ pid: short, ...res });
-      counts.processed++;
-
-      if (res.status === 'skip') {
-        counts.skip++;
-        console.log(`[${i+1}/40] ${short} SKIP: ${res.reason}`);
-      } else if (res.status === 'unknown') {
-        counts.unknown++;
-        console.log(`[${i+1}/40] ${short} UNKNOWN: ${res.reason}`);
-      } else {
-        if (res.group === 'C2') counts.c2++;
-        else if (res.group === 'C3') counts.c3++;
-        else if (res.group === 'C4') counts.c4++;
-        const changed = res.previousGroup !== res.group ? ` (was ${res.previousGroup})` : '';
-        console.log(`[${i+1}/40] ${short} OK: ${res.group}${changed} @ $${res.priceM2}/m2 — ${res.title}`);
-      }
-    } catch (e) {
-      counts.failed++;
-      results.push({ pid: short, status: 'failed', error: e.message });
-      console.error(`[${i+1}/40] ${short} FAILED: ${e.message}`);
-      notes.push(`${short}: ${e.message.slice(0, 80)}`);
-    }
-
-    // Rate limit: ~2 req/s for Shopify, plus RW fetch
-    if (i < PRODUCT_IDS.length - 1) await sleep(500);
-  }
-
-  const summary = {
-    processed: counts.processed,
-    c2: counts.c2,
-    c3: counts.c3,
-    c4: counts.c4,
-    unknown: counts.unknown,
-    failed: counts.failed,
-    skip: counts.skip,
-    notes: notes.slice(0, 10).join(' | ')
-  };
-  console.log('\n=== FINAL SUMMARY ===');
-  console.log(JSON.stringify(summary, null, 2));
-  return summary;
-}
-
-main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
diff --git a/rw-price-group-fix.js b/rw-price-group-fix.js
deleted file mode 100644
index c443de7..0000000
--- a/rw-price-group-fix.js
+++ /dev/null
@@ -1,302 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-fix.js
- * Determine Rebel Walls mural price group (C2/C3/C4) from live per-m2 retail rate
- * and correct cost metafields in Shopify sandbox.
- */
-
-const https = require('https');
-const http = require('http');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-// Price group buckets (per m2)
-// C2 ~58.10, C3 ~76.40, C4 ~88.30
-const PRICE_GROUPS = [
-  { name: 'C2', target: 58.10 },
-  { name: 'C3', target: 76.40 },
-  { name: 'C4', target: 88.30 },
-];
-
-// Pre-computed cost metafields per group
-const GROUP_DATA = {
-  C2: {
-    cost_per_m2: '33.84',
-    material_options: JSON.stringify([
-      { material: 'Non-Woven (Standard)', retail_per_m2: 58.10, cost_per_m2: 33.84, cost_confirmed: false },
-      { material: 'Peel & Stick', retail_per_m2: 69.70, cost_per_m2: 40.60, cost_confirmed: false },
-      { material: 'Commercial Grade', retail_per_m2: 81.36, cost_per_m2: 47.39, cost_confirmed: false },
-    ]),
-    cost_basis: 'C2 standard; portal-RRP-derived; P&S/Commercial extrapolated from RRP ratio',
-  },
-  C3: null, // leave as-is
-  C4: {
-    cost_per_m2: '51.44',
-    material_options: JSON.stringify([
-      { material: 'Non-Woven (Standard)', retail_per_m2: 88.30, cost_per_m2: 51.44, cost_confirmed: false },
-      { material: 'Peel & Stick', retail_per_m2: 106.00, cost_per_m2: 61.75, cost_confirmed: false },
-      { material: 'Commercial Grade', retail_per_m2: 123.66, cost_per_m2: 72.03, cost_confirmed: false },
-    ]),
-    cost_basis: 'C4 standard; portal-RRP-derived; P&S/Commercial extrapolated from RRP ratio',
-  },
-};
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-// Shopify GraphQL
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data),
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const lowBudget = (result?.extensions?.cost?.throttleStatus?.currentlyAvailable || 9999) < 200;
-      if (throttled) { await sleep(3000); continue; }
-      if (lowBudget) await sleep(1500);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-// Fetch a URL and return body text
-function fetchUrl(url) {
-  return new Promise((resolve, reject) => {
-    const urlObj = new URL(url);
-    const lib = urlObj.protocol === 'https:' ? https : http;
-    const req = lib.request({
-      hostname: urlObj.hostname,
-      path: urlObj.pathname + urlObj.search,
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.9',
-      },
-    }, res => {
-      let c = '';
-      // Handle redirects
-      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-        const redirectUrl = res.headers.location.startsWith('http')
-          ? res.headers.location
-          : `${urlObj.protocol}//${urlObj.hostname}${res.headers.location}`;
-        fetchUrl(redirectUrl).then(resolve).catch(reject);
-        return;
-      }
-      res.on('data', d => c += d);
-      res.on('end', () => resolve({ statusCode: res.statusCode, body: c }));
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error(`timeout fetching ${url}`)); });
-    req.end();
-  });
-}
-
-// Extract per-m2 price from RW product page
-// RW product pages embed the per-sqft price in JSON strings as: "$7.10 \\/ sq ft"
-// (double-encoded: backslash + forward slash). In the raw HTML this appears as
-// "$7.10 \\\\\\/ sq ft" (multiple backslash escapes). We match with [\\]*\/ to
-// handle any level of encoding.
-// JSON-LD "price":"N" is per-PANEL for large murals (e.g. $150), NOT per-m2.
-// Strategy: prefer sqft pattern (convert x10.7639); JSON-LD only if in 40-100 range.
-function extractPricePerM2(html) {
-  // Strategy 1 (PREFERRED): dollar amount followed by any backslashes then / then sq
-  // Catches: "$7.10 \/ sq", "$7.10 \\\/ sq", "$5.40 / sq ft", etc.
-  const re = /\$([\d.]+)\s*[\\]*\/\s*sq/gi;
-  for (const m of html.matchAll(re)) {
-    const v = parseFloat(m[1]);
-    // Sqft price for RW murals is ~3-15 range
-    if (v >= 3 && v <= 15) return v * 10.7639;
-  }
-
-  // Strategy 2: JSON-LD "price":"N","priceCurrency":"USD" — only if in per-m2 range (40-100)
-  // Values > 100 are per-panel prices for large murals (e.g. $150), not per-m2
-  const jsonLdM = html.match(/"price":\s*"([\d.]+)",\s*"priceCurrency":\s*"USD"/);
-  if (jsonLdM) {
-    const price = parseFloat(jsonLdM[1]);
-    if (price >= 40 && price <= 100) return price;
-  }
-
-  // Strategy 3: any "price" value in the 40-100 m2 range (fallback only)
-  for (const a of html.matchAll(/"price":\s*["']?([\d.]+)["']?/g)) {
-    const v = parseFloat(a[1]);
-    if (v >= 40 && v <= 100) return v;
-  }
-
-  // Strategy 4: explicit m2 patterns
-  const m2Patterns = [
-    /\$\s*([\d,]+\.?\d*)\s*\/\s*m[²2]/i,
-    /from\s+\$\s*([\d,]+\.?\d*)\s*\/\s*m[²2]/i,
-  ];
-  for (const pat of m2Patterns) {
-    const mm = html.match(pat);
-    if (mm) {
-      const price = parseFloat(mm[1].replace(',', ''));
-      if (price >= 40 && price <= 100) return price;
-    }
-  }
-
-  return null;
-}
-
-// Bucket price to group
-function bucketGroup(pricePerM2) {
-  if (!pricePerM2 || pricePerM2 <= 0) return null;
-  let best = null;
-  let bestDist = Infinity;
-  for (const g of PRICE_GROUPS) {
-    const dist = Math.abs(pricePerM2 - g.target);
-    if (dist < bestDist) { bestDist = dist; best = g.name; }
-  }
-  // Sanity check: if price is way off from all buckets (>25% from nearest), flag as unknown
-  const nearestTarget = PRICE_GROUPS.find(g => g.name === best)?.target || 76.40;
-  if (Math.abs(pricePerM2 - nearestTarget) / nearestTarget > 0.30) {
-    console.log(`  WARNING: price ${pricePerM2.toFixed(2)}/m² is >30% from nearest bucket (${best}=${nearestTarget}); using unknown`);
-    return null;
-  }
-  return best;
-}
-
-const MF_MUTATION = 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message field } } }';
-
-async function processProduct(pid) {
-  // 1. Fetch rw_product_url and existing price_group
-  const r = await gql({ query: `{ product(id:"${pid}") {
-    rw_product_url: metafield(namespace:"custom", key:"rw_product_url") { value }
-    pg: metafield(namespace:"custom", key:"price_group") { value }
-  }}` });
-
-  const prod = r?.data?.product;
-  if (!prod) throw new Error(`product not found`);
-
-  const rwUrl = prod.rw_product_url?.value;
-  const existingGroup = prod.pg?.value;
-
-  if (!rwUrl) {
-    console.log(`  ${pid}: no rw_product_url -> skip (unknown)`);
-    return { result: 'unknown', group: null };
-  }
-
-  // 2. Fetch the RW product page
-  let pricePerM2 = null;
-  try {
-    const { statusCode, body } = await fetchUrl(rwUrl);
-    if (statusCode !== 200) {
-      console.log(`  ${pid}: HTTP ${statusCode} fetching ${rwUrl} -> unknown`);
-      return { result: 'unknown', group: null };
-    }
-    pricePerM2 = extractPricePerM2(body);
-    if (pricePerM2) {
-      console.log(`  ${pid}: fetched ${rwUrl} -> ${pricePerM2.toFixed(2)}/m²`);
-    } else {
-      console.log(`  ${pid}: could not extract price from ${rwUrl}`);
-      // Debug: show first 2000 chars of relevant parts
-      const pricePart = body.match(/price[\s\S]{0,200}/i);
-      if (pricePart) console.log(`  DEBUG price context: ${pricePart[0].slice(0, 200).replace(/\n/g, ' ')}`);
-    }
-  } catch (e) {
-    console.log(`  ${pid}: fetch error ${e.message} -> unknown`);
-    return { result: 'unknown', group: null };
-  }
-
-  const group = bucketGroup(pricePerM2);
-  if (!group) {
-    console.log(`  ${pid}: price unknown (extracted: ${pricePerM2?.toFixed(2) || 'null'}) -> keeping ${existingGroup || 'C3'}`);
-    return { result: 'unknown', group: null };
-  }
-
-  console.log(`  ${pid}: group=${group} (price=${pricePerM2.toFixed(2)}/m², existing=${existingGroup || 'none'})`);
-
-  // 3. Write price_group metafield
-  const mfs = [
-    { ownerId: pid, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-  ];
-
-  // 4. If not C3, overwrite cost metafields
-  if (group !== 'C3') {
-    const gd = GROUP_DATA[group];
-    mfs.push(
-      { ownerId: pid, namespace: 'custom', key: 'cost_per_m2', value: gd.cost_per_m2, type: 'number_decimal' },
-      { ownerId: pid, namespace: 'custom', key: 'material_options', value: gd.material_options, type: 'json' },
-      { ownerId: pid, namespace: 'custom', key: 'cost_basis', value: gd.cost_basis, type: 'single_line_text_field' },
-    );
-  }
-
-  const wr = await gql({ query: MF_MUTATION, variables: { m: mfs } });
-  const errs = wr?.data?.metafieldsSet?.userErrors || [];
-  if (errs.length) {
-    throw new Error(`metafieldsSet errors: ${errs.map(e => e.message).join('; ')}`);
-  }
-  console.log(`  ${pid}: wrote price_group=${group}${group !== 'C3' ? ` + cost metafields` : ''} OK`);
-
-  return { result: 'ok', group };
-}
-
-// Run 2: only the 5 products that were "unknown" in run 1 (all others already written)
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7851168006195',  // alpaca-rebel-pink   -> $150 per panel, sqft=$7.10 -> C3
-  'gid://shopify/Product/7851168038963',  // alpaca-rebel-rainbow
-  'gid://shopify/Product/7851168071731',  // alpaca-rebel-sky
-  'gid://shopify/Product/7851168137267',  // amazon-fern
-  'gid://shopify/Product/7851168268339',  // anglessy-plum
-];
-
-async function main() {
-  let processed = 0, c2 = 0, c3 = 0, c4 = 0, unknown = 0, failed = 0;
-  const notes = [];
-
-  for (const pid of PRODUCT_IDS) {
-    try {
-      const { result, group } = await processProduct(pid);
-      processed++;
-      if (result === 'unknown') {
-        unknown++;
-      } else if (group === 'C2') {
-        c2++;
-      } else if (group === 'C3') {
-        c3++;
-      } else if (group === 'C4') {
-        c4++;
-      }
-    } catch (e) {
-      failed++;
-      processed++;
-      console.log(`  FAILED ${pid}: ${e.message}`);
-      notes.push(`FAIL ${pid}: ${e.message}`);
-    }
-    await sleep(500); // ~2 req/s
-  }
-
-  const summary = { processed, c2, c3, c4, unknown, failed, notes: notes.join(' | ') || 'none' };
-  console.log('\n=== FINAL RESULT ===');
-  console.log(JSON.stringify(summary, null, 2));
-  return summary;
-}
-
-main().catch(e => { console.error(`FATAL: ${e.message}`); process.exit(1); });
diff --git a/rw-price-group-offset520.js b/rw-price-group-offset520.js
deleted file mode 100644
index 8f4c62a..0000000
--- a/rw-price-group-offset520.js
+++ /dev/null
@@ -1,403 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-offset520.js
- * Determine Rebel Walls price group (C2/C3/C4) from live RW product page,
- * then write price_group + cost metafields to sandbox Shopify.
- * Slice: LIMIT 40 OFFSET 520
- */
-
-const https = require('https');
-const http = require('http');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7855821652019',
-  'gid://shopify/Product/7855821684787',
-  'gid://shopify/Product/7855821717555',
-  'gid://shopify/Product/7855821750323',
-  'gid://shopify/Product/7855821783091',
-  'gid://shopify/Product/7855821815859',
-  'gid://shopify/Product/7855821848627',
-  'gid://shopify/Product/7855821881395',
-  'gid://shopify/Product/7855821914163',
-  'gid://shopify/Product/7855821946931',
-  'gid://shopify/Product/7855821979699',
-  'gid://shopify/Product/7855822012467',
-  'gid://shopify/Product/7855822045235',
-  'gid://shopify/Product/7855822078003',
-  'gid://shopify/Product/7855822110771',
-  'gid://shopify/Product/7855822143539',
-  'gid://shopify/Product/7855822176307',
-  'gid://shopify/Product/7855822209075',
-  'gid://shopify/Product/7855822241843',
-  'gid://shopify/Product/7855822274611',
-  'gid://shopify/Product/7855822307379',
-  'gid://shopify/Product/7855822340147',
-  'gid://shopify/Product/7855822405683',
-  'gid://shopify/Product/7855822438451',
-  'gid://shopify/Product/7855822471219',
-  'gid://shopify/Product/7855822503987',
-  'gid://shopify/Product/7855822536755',
-  'gid://shopify/Product/7855822569523',
-  'gid://shopify/Product/7855822602291',
-  'gid://shopify/Product/7855822635059',
-  'gid://shopify/Product/7855822667827',
-  'gid://shopify/Product/7855822700595',
-  'gid://shopify/Product/7855822766131',
-  'gid://shopify/Product/7855822798899',
-  'gid://shopify/Product/7855822864435',
-  'gid://shopify/Product/7855822929971',
-  'gid://shopify/Product/7855822962739',
-  'gid://shopify/Product/7855822995507',
-  'gid://shopify/Product/7855823028275',
-  'gid://shopify/Product/7855823061043',
-];
-
-// Price group data
-const PRICE_GROUPS = {
-  C2: {
-    cost_per_m2: '33.84',
-    material_options: JSON.stringify([
-      {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-      {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-      {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-    ]),
-    cost_basis: 'C2 group; portal-RRP-derived; P&S/Commercial extrapolated'
-  },
-  C3: null, // leave as-is
-  C4: {
-    cost_per_m2: '51.44',
-    material_options: JSON.stringify([
-      {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-      {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-      {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-    ]),
-    cost_basis: 'C4 group; portal-RRP-derived; P&S/Commercial extrapolated'
-  }
-};
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gql(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      }
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-function fetchUrl(url, redirectCount = 0) {
-  return new Promise((resolve, reject) => {
-    if (redirectCount > 5) return reject(new Error('too many redirects'));
-    const parsed = new URL(url);
-    const lib = parsed.protocol === 'https:' ? https : http;
-    const req = lib.request({
-      hostname: parsed.hostname,
-      path: parsed.pathname + parsed.search,
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.5',
-      }
-    }, res => {
-      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-        const redir = res.headers.location.startsWith('http')
-          ? res.headers.location
-          : `${parsed.protocol}//${parsed.hostname}${res.headers.location}`;
-        res.resume();
-        return resolve(fetchUrl(redir, redirectCount + 1));
-      }
-      let body = '';
-      res.on('data', d => body += d);
-      res.on('end', () => resolve({ status: res.statusCode, body }));
-    });
-    req.on('error', reject);
-    req.setTimeout(20000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.end();
-  });
-}
-
-function extractPricePerM2(html, url) {
-  // Try JSON-LD first
-  const jsonLdMatches = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const block of jsonLdMatches) {
-    const inner = block.replace(/<script[^>]*>/i, '').replace(/<\/script>/i, '').trim();
-    try {
-      const obj = JSON.parse(inner);
-      const offers = obj.offers || (obj['@graph'] && obj['@graph'].find(o => o.offers))?.offers;
-      if (offers) {
-        const price = offers.price || offers.lowPrice;
-        if (price) {
-          // Check if it has unit info
-          const pricePer = offers.priceCurrency === 'USD' ? parseFloat(price) : null;
-          if (pricePer && pricePer > 0) {
-            // JSON-LD price might be per roll, per panel, or per sqft/m2
-            // We'll check if this looks like a per-sqft rate
-            if (pricePer >= 4 && pricePer <= 15) {
-              // Likely per sqft, convert to m2
-              return pricePer * 10.7639;
-            }
-            if (pricePer >= 40 && pricePer <= 150) {
-              // Likely per m2
-              return pricePer;
-            }
-          }
-        }
-      }
-    } catch {}
-  }
-
-  // Try to find price in HTML text patterns
-  // RW shows "From $X.XX / sq ft" or "From $X.XX / m²"
-
-  // Pattern: per sq ft
-  const sqftPatterns = [
-    /\$\s*([\d]+\.[\d]{2})\s*\/\s*sq\.?\s*ft/gi,
-    /from\s+\$\s*([\d]+\.[\d]{2})\s*\/\s*sq/gi,
-    /([\d]+\.[\d]{2})\s*USD\s*\/\s*sq\.?\s*ft/gi,
-    /price[^>]*>\s*\$\s*([\d]+\.[\d]{2})\s*<[^>]*>\s*\/\s*sq/gi,
-  ];
-
-  for (const pat of sqftPatterns) {
-    const m = pat.exec(html);
-    if (m) {
-      const sqftPrice = parseFloat(m[1]);
-      if (sqftPrice >= 3 && sqftPrice <= 20) {
-        return sqftPrice * 10.7639;
-      }
-    }
-  }
-
-  // Pattern: per m²
-  const m2Patterns = [
-    /\$\s*([\d]+\.[\d]{2})\s*\/\s*m[²2]/gi,
-    /from\s+\$\s*([\d]+\.[\d]{2})\s*\/\s*m[²2]/gi,
-    /([\d]+\.[\d]{2})\s*USD\s*\/\s*m[²2]/gi,
-  ];
-
-  for (const pat of m2Patterns) {
-    const m = pat.exec(html);
-    if (m) {
-      const m2Price = parseFloat(m[1]);
-      if (m2Price >= 40 && m2Price <= 200) {
-        return m2Price;
-      }
-    }
-  }
-
-  // Try to find any price near "From" keyword
-  // Look for price chips in the page
-  const fromPrices = [];
-  const fromPatterns = [
-    /from[\s\S]{0,50}\$([\d]+\.[\d]{2})/gi,
-    /\$([\d]+\.[\d]{2})[\s\S]{0,30}\/\s*(sq\.?\s*ft|m[²2])/gi,
-    /"price"\s*:\s*"([\d]+\.[\d]{2})"/gi,
-    /"lowPrice"\s*:\s*"([\d]+\.[\d]{2})"/gi,
-  ];
-
-  for (const pat of fromPatterns) {
-    let m;
-    pat.lastIndex = 0;
-    while ((m = pat.exec(html)) !== null) {
-      const v = parseFloat(m[1]);
-      fromPrices.push(v);
-    }
-  }
-
-  // Filter to reasonable per-sqft range and take minimum (base price)
-  const sqftCandidates = fromPrices.filter(v => v >= 4 && v <= 20);
-  if (sqftCandidates.length > 0) {
-    const minSqft = Math.min(...sqftCandidates);
-    return minSqft * 10.7639;
-  }
-
-  // Filter to reasonable per-m2 range
-  const m2Candidates = fromPrices.filter(v => v >= 40 && v <= 200);
-  if (m2Candidates.length > 0) {
-    return Math.min(...m2Candidates);
-  }
-
-  return null;
-}
-
-function bucketPriceGroup(pricePerM2) {
-  if (pricePerM2 === null) return null;
-  // C2: ~58.10, C3: ~76.40, C4: ~88.30
-  // Midpoints: C2/C3 = 67.25, C3/C4 = 82.35
-  if (pricePerM2 <= 67.25) return 'C2';
-  if (pricePerM2 <= 82.35) return 'C3';
-  return 'C4';
-}
-
-async function fetchMetafields(productId) {
-  const res = await gql({
-    query: `query GetMeta($id: ID!) {
-      product(id: $id) {
-        rw_url: metafield(namespace: "custom", key: "rw_product_url") { value }
-        price_group: metafield(namespace: "custom", key: "price_group") { value }
-      }
-    }`,
-    variables: { id: productId }
-  });
-
-  if (res.errors || !res.data || !res.data.product) {
-    throw new Error(`GQL fetch failed: ${JSON.stringify(res.errors || res)}`);
-  }
-
-  return {
-    rw_url: res.data.product.rw_url?.value || null,
-    price_group: res.data.product.price_group?.value || null,
-  };
-}
-
-async function writeMetafields(productId, group) {
-  const pg = PRICE_GROUPS[group];
-  if (!pg) return; // C3, skip writes
-
-  const muts = [
-    { ownerId: productId, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-    { ownerId: productId, namespace: 'custom', key: 'cost_per_m2', value: pg.cost_per_m2, type: 'number_decimal' },
-    { ownerId: productId, namespace: 'custom', key: 'material_options', value: pg.material_options, type: 'json' },
-    { ownerId: productId, namespace: 'custom', key: 'cost_basis', value: pg.cost_basis, type: 'multi_line_text_field' },
-  ];
-
-  const res = await gql({
-    query: `mutation SetMeta($metas: [MetafieldsSetInput!]!) {
-      metafieldsSet(metafields: $metas) {
-        metafields { key value }
-        userErrors { field message }
-      }
-    }`,
-    variables: { metas: muts }
-  });
-
-  const errs = res.data?.metafieldsSet?.userErrors || [];
-  if (errs.length > 0) throw new Error(`userErrors: ${JSON.stringify(errs)}`);
-  if (res.errors) throw new Error(`GQL errors: ${JSON.stringify(res.errors)}`);
-
-  return res.data?.metafieldsSet?.metafields?.length || 0;
-}
-
-async function writePriceGroupOnly(productId, group) {
-  // For C3 or when we only need to set price_group
-  const res = await gql({
-    query: `mutation SetMeta($metas: [MetafieldsSetInput!]!) {
-      metafieldsSet(metafields: $metas) {
-        metafields { key value }
-        userErrors { field message }
-      }
-    }`,
-    variables: {
-      metas: [{ ownerId: productId, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' }]
-    }
-  });
-
-  const errs = res.data?.metafieldsSet?.userErrors || [];
-  if (errs.length > 0) throw new Error(`userErrors: ${JSON.stringify(errs)}`);
-}
-
-async function processProduct(productId, idx) {
-  const shortId = productId.replace('gid://shopify/Product/', '');
-
-  try {
-    // Step 1: fetch existing metafields
-    const meta = await fetchMetafields(productId);
-
-    if (!meta.rw_url) {
-      console.log(`[${idx}] ${shortId} — no rw_product_url → SKIP`);
-      return { result: 'skip_no_url' };
-    }
-
-    // Step 2: fetch the RW product page
-    let html;
-    try {
-      const resp = await fetchUrl(meta.rw_url);
-      if (resp.status !== 200) {
-        console.log(`[${idx}] ${shortId} — HTTP ${resp.status} for ${meta.rw_url} → unknown`);
-        return { result: 'unknown', reason: `HTTP ${resp.status}` };
-      }
-      html = resp.body;
-    } catch (e) {
-      console.log(`[${idx}] ${shortId} — fetch error: ${e.message} → unknown`);
-      return { result: 'unknown', reason: e.message };
-    }
-
-    // Step 3: extract price
-    const pricePerM2 = extractPricePerM2(html, meta.rw_url);
-    const group = bucketPriceGroup(pricePerM2);
-
-    if (!group) {
-      console.log(`[${idx}] ${shortId} — could not extract price from ${meta.rw_url} → unknown (existing pg: ${meta.price_group || 'none'})`);
-      return { result: 'unknown', reason: 'no price extracted' };
-    }
-
-    console.log(`[${idx}] ${shortId} — $${pricePerM2?.toFixed(2)}/m2 → ${group} (was: ${meta.price_group || 'none'})`);
-
-    // Step 4: write metafields
-    if (group !== 'C3') {
-      await writeMetafields(productId, group);
-    } else {
-      // Write price_group=C3 but leave cost fields as-is
-      await writePriceGroupOnly(productId, 'C3');
-    }
-
-    return { result: 'ok', group };
-
-  } catch (e) {
-    console.error(`[${idx}] ${shortId} — FAILED: ${e.message}`);
-    return { result: 'failed', reason: e.message };
-  }
-}
-
-async function main() {
-  const stats = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0, skip: 0 };
-  const notes = [];
-
-  for (let i = 0; i < PRODUCT_IDS.length; i++) {
-    const pid = PRODUCT_IDS[i];
-    const res = await processProduct(pid, i + 1);
-
-    stats.processed++;
-
-    if (res.result === 'ok') {
-      if (res.group === 'C2') stats.c2++;
-      else if (res.group === 'C3') stats.c3++;
-      else if (res.group === 'C4') stats.c4++;
-    } else if (res.result === 'skip_no_url') {
-      stats.skip++;
-    } else if (res.result === 'unknown') {
-      stats.unknown++;
-      notes.push(`unknown: ${pid.slice(-13)}: ${res.reason}`);
-    } else if (res.result === 'failed') {
-      stats.failed++;
-      notes.push(`failed: ${pid.slice(-13)}: ${res.reason}`);
-    }
-
-    // ~2 req/s (fetch + write = ~2 calls per product, so 500ms gap)
-    if (i < PRODUCT_IDS.length - 1) await sleep(500);
-  }
-
-  console.log('\n=== FINAL RESULTS ===');
-  console.log(JSON.stringify({ ...stats, notes: notes.slice(0, 10) }, null, 2));
-}
-
-main().catch(e => { console.error('FATAL:', e); process.exit(1); });
diff --git a/rw-price-group-slice5.js b/rw-price-group-slice5.js
deleted file mode 100644
index ec022ec..0000000
--- a/rw-price-group-slice5.js
+++ /dev/null
@@ -1,413 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-slice5.js
- * Slice: OFFSET 200, LIMIT 40 — Rebel Walls price-group bucketing
- * Reads rw_product_url metafield, fetches RW page, extracts base per-m2 price,
- * buckets C2/C3/C4, writes price_group + cost metafields to sandbox.
- */
-
-const https = require('https');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API   = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7851171479603',
-  'gid://shopify/Product/7851171512371',
-  'gid://shopify/Product/7851171545139',
-  'gid://shopify/Product/7851171643443',
-  'gid://shopify/Product/7851171577907',
-  'gid://shopify/Product/7851171610675',
-  'gid://shopify/Product/7851171676211',
-  'gid://shopify/Product/7851171708979',
-  'gid://shopify/Product/7851171741747',
-  'gid://shopify/Product/7851171774515',
-  'gid://shopify/Product/7851171807283',
-  'gid://shopify/Product/7851171840051',
-  'gid://shopify/Product/7851171872819',
-  'gid://shopify/Product/7851171905587',
-  'gid://shopify/Product/7851171938355',
-  'gid://shopify/Product/7851171971123',
-  'gid://shopify/Product/7851172003891',
-  'gid://shopify/Product/7851172036659',
-  'gid://shopify/Product/7851172069427',
-  'gid://shopify/Product/7851172102195',
-  'gid://shopify/Product/7851172134963',
-  'gid://shopify/Product/7851172167731',
-  'gid://shopify/Product/7851172200499',
-  'gid://shopify/Product/7851172233267',
-  'gid://shopify/Product/7851172266035',
-  'gid://shopify/Product/7851172298803',
-  'gid://shopify/Product/7851172331571',
-  'gid://shopify/Product/7851172364339',
-  'gid://shopify/Product/7851172397107',
-  'gid://shopify/Product/7851172429875',
-  'gid://shopify/Product/7851172462643',
-  'gid://shopify/Product/7851172495411',
-  'gid://shopify/Product/7851172528179',
-  'gid://shopify/Product/7851172560947',
-  'gid://shopify/Product/7851172593715',
-  'gid://shopify/Product/7851172626483',
-  'gid://shopify/Product/7851172659251',
-  'gid://shopify/Product/7851172724787',
-  'gid://shopify/Product/7851172757555',
-  'gid://shopify/Product/7851172790323',
-];
-
-// Pre-computed material_options per group
-const MATERIAL_OPTIONS = {
-  C2: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-  ]),
-  C3: null, // don't rewrite
-  C4: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-  ]),
-};
-
-const COST_PER_M2 = { C2: '33.84', C3: '44.50', C4: '51.44' };
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data),
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => {
-        try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); }
-      });
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const lowBudget = (result?.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 9999) < 200;
-      if (throttled) { await sleep(3000); continue; }
-      if (lowBudget) await sleep(1200);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-function fetchUrl(urlStr, retries = 3) {
-  return new Promise((resolve, reject) => {
-    const tryFetch = (url, attempt) => {
-      try {
-        const u = new URL(url);
-        const mod = u.protocol === 'https:' ? https : require('http');
-        const req = mod.request({
-          hostname: u.hostname,
-          path: u.pathname + u.search,
-          method: 'GET',
-          headers: {
-            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
-            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-            'Accept-Language': 'en-US,en;q=0.5',
-          },
-        }, res => {
-          // Follow redirects
-          if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-            const loc = res.headers.location;
-            const nextUrl = loc.startsWith('http') ? loc : `${u.protocol}//${u.hostname}${loc}`;
-            res.destroy();
-            if (attempt < retries + 3) tryFetch(nextUrl, attempt + 1);
-            else reject(new Error(`Too many redirects from ${url}`));
-            return;
-          }
-          let body = '';
-          res.on('data', d => body += d);
-          res.on('end', () => resolve({ status: res.statusCode, body }));
-        });
-        req.on('error', e => {
-          if (attempt < retries) { sleep(attempt * 1000).then(() => tryFetch(url, attempt + 1)); }
-          else reject(e);
-        });
-        req.setTimeout(20000, () => { req.destroy(); reject(new Error(`fetch timeout: ${url}`)); });
-        req.end();
-      } catch (e) { reject(e); }
-    };
-    tryFetch(urlStr, 1);
-  });
-}
-
-/**
- * Extract the base (cheapest) per-m2 price from a Rebel Walls product page.
- * RW shows prices in USD. The base Non-Woven price is what we bucket on.
- * Returns a number (USD per m2) or null if not found.
- */
-function extractPricePerM2(html, url) {
-  // Strategy 1: JSON-LD structured data
-  const jsonLdMatches = html.match(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const block of jsonLdMatches) {
-    const inner = block.replace(/<script[^>]*>/i, '').replace(/<\/script>/i, '');
-    try {
-      const parsed = JSON.parse(inner);
-      const items = Array.isArray(parsed) ? parsed : [parsed];
-      for (const item of items) {
-        const offers = item.offers || (item['@graph'] || []).flatMap(n => n.offers || []);
-        const offerArr = Array.isArray(offers) ? offers : [offers];
-        for (const offer of offerArr) {
-          if (offer && offer.price) {
-            const p = parseFloat(String(offer.price).replace(/[^0-9.]/g, ''));
-            if (!isNaN(p) && p > 0) {
-              // Determine if it's per sqft or per m2
-              const unit = String(offer.unitCode || offer.unitText || '').toLowerCase();
-              if (unit.includes('sqft') || unit.includes('sq ft') || unit.includes('ft')) {
-                return Math.round(p * 10.7639 * 100) / 100;
-              }
-              // Assume per m2 if in the range we expect (50-200)
-              if (p >= 30 && p <= 300) return p;
-            }
-          }
-        }
-      }
-    } catch { /* skip */ }
-  }
-
-  // Strategy 2: Look for "From $X / sq ft" or "From $X/m²" patterns
-  // RW US site shows prices in USD per sq ft
-  const sqftPatterns = [
-    /[Ff]rom\s*\$\s*([\d,.]+)\s*\/\s*sq\.?\s*ft/i,
-    /\$\s*([\d,.]+)\s*\/\s*sq\.?\s*ft/i,
-    /[Ff]rom\s*\$\s*([\d,.]+)\s*per\s*sq\.?\s*ft/i,
-    /price[^<]*\$\s*([\d,.]+)\s*\/\s*sq/i,
-  ];
-  for (const pat of sqftPatterns) {
-    const m = html.match(pat);
-    if (m) {
-      const p = parseFloat(m[1].replace(/,/g, ''));
-      if (!isNaN(p) && p > 0) {
-        return Math.round(p * 10.7639 * 100) / 100;
-      }
-    }
-  }
-
-  // Strategy 3: per-m2 patterns
-  const m2Patterns = [
-    /[Ff]rom\s*\$\s*([\d,.]+)\s*\/\s*m[²2]/i,
-    /\$\s*([\d,.]+)\s*\/\s*m[²2]/i,
-    /[Ff]rom\s*\$\s*([\d,.]+)\s*per\s*m[²2]/i,
-  ];
-  for (const pat of m2Patterns) {
-    const m = html.match(pat);
-    if (m) {
-      const p = parseFloat(m[1].replace(/,/g, ''));
-      if (!isNaN(p) && p > 0) return p;
-    }
-  }
-
-  // Strategy 4: Look for price in page data / React state / window.__INITIAL_STATE__
-  // RW often embeds pricing in a script tag
-  const priceInScript = html.match(/['"](price|basePrice|pricePerSqFt|pricePerM2)['"]\s*:\s*["']?([\d.]+)/gi) || [];
-  for (const match of priceInScript) {
-    const numM = match.match(/([\d.]+)$/);
-    if (numM) {
-      const p = parseFloat(numM[1]);
-      if (!isNaN(p) && p > 0 && p < 1000) {
-        // Heuristic: values < 30 are likely per sqft
-        if (p < 30) return Math.round(p * 10.7639 * 100) / 100;
-        if (p >= 30 && p <= 300) return p;
-      }
-    }
-  }
-
-  // Strategy 5: Look for any dollar amount near "sqft" text within 100 chars
-  const sqftNearby = [...html.matchAll(/\$([\d,.]+)[^<]{0,60}sq\s*ft/gi)];
-  for (const m of sqftNearby) {
-    const p = parseFloat(m[1].replace(/,/g, ''));
-    if (!isNaN(p) && p > 0 && p < 50) {
-      return Math.round(p * 10.7639 * 100) / 100;
-    }
-  }
-
-  return null;
-}
-
-/**
- * Bucket a USD/m2 price into C2/C3/C4.
- * Thresholds: C2 ~$58.10, C3 ~$76.40, C4 ~$88.30
- * Use midpoints between groups: <67.25 => C2, 67.25-82.35 => C3, >82.35 => C4
- */
-function bucketPrice(pricePerM2) {
-  if (pricePerM2 === null) return null;
-  if (pricePerM2 < 67.25) return 'C2';
-  if (pricePerM2 < 82.35) return 'C3';
-  return 'C4';
-}
-
-async function fetchProductMetafields(productId) {
-  const q = {
-    query: `query($id: ID!) {
-      product(id: $id) {
-        title
-        url: metafield(namespace: "custom", key: "rw_product_url") { value }
-        pg:  metafield(namespace: "custom", key: "price_group")     { value }
-      }
-    }`,
-    variables: { id: productId },
-  };
-  const res = await gql(q);
-  const p = res?.data?.product;
-  if (!p) return null;
-  return {
-    title: p.title,
-    url: p.url?.value || null,
-    price_group: p.pg?.value || null,
-  };
-}
-
-async function writeMetafields(productId, group) {
-  // Build metafield array
-  const metafields = [
-    { ownerId: productId, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-    { ownerId: productId, namespace: 'custom', key: 'cost_basis',  value: `${group} — portal-RRP-derived, P&S/Commercial extrapolated`, type: 'single_line_text_field' },
-  ];
-
-  if (group !== 'C3') {
-    metafields.push({
-      ownerId: productId, namespace: 'custom', key: 'cost_per_m2',
-      value: COST_PER_M2[group], type: 'number_decimal',
-    });
-    metafields.push({
-      ownerId: productId, namespace: 'custom', key: 'material_options',
-      value: MATERIAL_OPTIONS[group], type: 'json',
-    });
-  }
-
-  const mutation = {
-    query: `mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
-      metafieldsSet(metafields: $metafields) {
-        metafields { key value }
-        userErrors  { field message }
-      }
-    }`,
-    variables: { metafields },
-  };
-
-  const res = await gql(mutation);
-  const userErrors = res?.data?.metafieldsSet?.userErrors || [];
-  if (res?.errors?.length) throw new Error(JSON.stringify(res.errors));
-  if (userErrors.length) throw new Error(JSON.stringify(userErrors));
-  return res?.data?.metafieldsSet?.metafields?.length || 0;
-}
-
-async function processProduct(productId, idx, total) {
-  const shortId = productId.split('/').pop();
-  const prefix = `[${idx+1}/${total} id:${shortId}]`;
-
-  try {
-    // Step 1: fetch metafields
-    const meta = await fetchProductMetafields(productId);
-    if (!meta) {
-      console.log(`${prefix} SKIP: product not found`);
-      return { result: 'skip', group: null };
-    }
-
-    if (!meta.url) {
-      console.log(`${prefix} SKIP: no rw_product_url — "${meta.title}"`);
-      return { result: 'skip_no_url', group: null };
-    }
-
-    console.log(`${prefix} url=${meta.url} existing_pg=${meta.price_group || 'none'}`);
-
-    // Step 2: fetch RW page
-    let html;
-    try {
-      const resp = await fetchUrl(meta.url);
-      if (resp.status !== 200) {
-        console.log(`${prefix} WARN: HTTP ${resp.status} from ${meta.url}`);
-        if (resp.status === 404 || resp.status === 410) {
-          return { result: 'unknown', group: null };
-        }
-      }
-      html = resp.body;
-    } catch (e) {
-      console.log(`${prefix} WARN: fetch failed: ${e.message}`);
-      return { result: 'unknown', group: null };
-    }
-
-    // Step 3: extract price
-    const priceM2 = extractPricePerM2(html, meta.url);
-    console.log(`${prefix} extracted price/m2=${priceM2}`);
-
-    const group = bucketPrice(priceM2);
-    if (!group) {
-      console.log(`${prefix} UNKNOWN: could not extract price from ${meta.url}`);
-      return { result: 'unknown', group: null };
-    }
-
-    console.log(`${prefix} bucket=${group}`);
-
-    // Step 4: write metafields
-    await writeMetafields(productId, group);
-    console.log(`${prefix} WRITTEN group=${group}`);
-
-    return { result: 'ok', group };
-  } catch (e) {
-    console.error(`${prefix} FAILED: ${e.message}`);
-    return { result: 'failed', group: null };
-  }
-}
-
-async function main() {
-  console.log(`Starting Rebel Walls price-group bucketing — slice 5 (offset 200, limit 40)`);
-  console.log(`Products: ${PRODUCT_IDS.length}`);
-
-  const counts = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0, skip: 0 };
-
-  for (let i = 0; i < PRODUCT_IDS.length; i++) {
-    const res = await processProduct(PRODUCT_IDS[i], i, PRODUCT_IDS.length);
-    counts.processed++;
-    if (res.result === 'ok') {
-      if (res.group === 'C2') counts.c2++;
-      else if (res.group === 'C3') counts.c3++;
-      else if (res.group === 'C4') counts.c4++;
-    } else if (res.result === 'unknown') {
-      counts.unknown++;
-    } else if (res.result === 'failed') {
-      counts.failed++;
-    } else {
-      counts.skip++;
-      counts.processed--; // don't count skips in processed
-    }
-    // ~2 req/s pacing
-    if (i < PRODUCT_IDS.length - 1) await sleep(500);
-  }
-
-  console.log('\n=== FINAL RESULTS ===');
-  console.log(JSON.stringify(counts, null, 2));
-  return counts;
-}
-
-main().then(counts => {
-  process.exit(0);
-}).catch(e => {
-  console.error('FATAL:', e);
-  process.exit(1);
-});
diff --git a/rw-price-group.js b/rw-price-group.js
deleted file mode 100644
index f6f7265..0000000
--- a/rw-price-group.js
+++ /dev/null
@@ -1,363 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group.js — Determine Rebel Walls price group (C2/C3/C4) from live RW product page
- * and update cost metafields on Shopify sandbox store.
- *
- * Buckets:
- *   ~58.10/m2 => C2 (Non-Woven Standard cheaper tier)
- *   ~76.40/m2 => C3 (baseline default)
- *   ~88.30/m2 => C4 (premium)
- *
- * Reads SHOPIFY_ADMIN_TOKEN from env.
- * REPLACED by new implementation below.
- * SANDBOX only. ~2 req/s.
- */
-
-const https = require('https');
-const http  = require('http');
-const { URL } = require('url');
-
-const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_PRODUCT_TOKEN;
-const API   = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-// Product IDs from query (OFFSET 320 LIMIT 40)
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7851176132659',
-  'gid://shopify/Product/7851176165427',
-  'gid://shopify/Product/7851176198195',
-  'gid://shopify/Product/7851176263731',
-  'gid://shopify/Product/7851176296499',
-  'gid://shopify/Product/7851176362035',
-  'gid://shopify/Product/7851176394803',
-  'gid://shopify/Product/7851176427571',
-  'gid://shopify/Product/7851176460339',
-  'gid://shopify/Product/7851176525875',
-  'gid://shopify/Product/7851176493107',
-  'gid://shopify/Product/7851176558643',
-  'gid://shopify/Product/7851176624179',
-  'gid://shopify/Product/7851176591411',
-  'gid://shopify/Product/7851176722483',
-  'gid://shopify/Product/7851176755251',
-  'gid://shopify/Product/7851176853555',
-  'gid://shopify/Product/7851177082931',
-  'gid://shopify/Product/7851177115699',
-  'gid://shopify/Product/7851176689715',
-  'gid://shopify/Product/7851176788019',
-  'gid://shopify/Product/7851176820787',
-  'gid://shopify/Product/7851176886323',
-  'gid://shopify/Product/7851176919091',
-  'gid://shopify/Product/7851176951859',
-  'gid://shopify/Product/7851176984627',
-  'gid://shopify/Product/7851177017395',
-  'gid://shopify/Product/7851177050163',
-  'gid://shopify/Product/7851177148467',
-  'gid://shopify/Product/7851177181235',
-  'gid://shopify/Product/7851177214003',
-  'gid://shopify/Product/7851177246771',
-  'gid://shopify/Product/7851177279539',
-  'gid://shopify/Product/7851177312307',
-  'gid://shopify/Product/7851177345075',
-  'gid://shopify/Product/7851177377843',
-  'gid://shopify/Product/7851177410611',
-  'gid://shopify/Product/7851177443379',
-  'gid://shopify/Product/7851177476147',
-  'gid://shopify/Product/7851177508915',
-];
-
-// Per-group material_options pre-computed
-const MATERIAL_OPTIONS = {
-  C2: [
-    {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-  ],
-  C3: [
-    {"material":"Non-Woven (Standard)","retail_per_m2":76.40,"cost_per_m2":44.50,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":91.68,"cost_per_m2":53.40,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":107.00,"cost_per_m2":62.33,"cost_confirmed":false}
-  ],
-  C4: [
-    {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-  ]
-};
-
-const GROUP_BASE_RETAIL = { C2: 33.84, C3: 44.50, C4: 51.44 }; // cost_per_m2
-const GROUP_RETAIL = { C2: 58.10, C3: 76.40, C4: 88.30 };
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-// ---------- Shopify GraphQL ----------
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const avail = result?.extensions?.cost?.throttleStatus?.currentlyAvailable;
-      if (throttled) { await sleep(3000); continue; }
-      if (avail != null && avail < 200) await sleep(1500);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-// ---------- HTTP fetch with redirect following ----------
-function fetchUrl(rawUrl, redirectCount = 0) {
-  return new Promise((resolve, reject) => {
-    if (redirectCount > 5) return reject(new Error('too many redirects'));
-    let parsed;
-    try { parsed = new URL(rawUrl); } catch { return reject(new Error(`bad URL: ${rawUrl}`)); }
-    const mod = parsed.protocol === 'https:' ? https : http;
-    const req = mod.request({
-      hostname: parsed.hostname,
-      path: parsed.pathname + (parsed.search || ''),
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120',
-        'Accept': 'text/html,application/xhtml+xml,*/*',
-        'Accept-Language': 'en-US,en;q=0.9',
-      }
-    }, res => {
-      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-        const next = res.headers.location.startsWith('http')
-          ? res.headers.location
-          : `${parsed.protocol}//${parsed.hostname}${res.headers.location}`;
-        res.resume();
-        return resolve(fetchUrl(next, redirectCount + 1));
-      }
-      let body = '';
-      res.on('data', d => body += d);
-      res.on('end', () => resolve({ status: res.statusCode, body }));
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.end();
-  });
-}
-
-// ---------- Price extraction from RW page ----------
-// RW shows prices like "From $5.40 / sq ft" or "From $58 / m²"
-// Also check JSON-LD for offers
-function extractPricePerM2(html) {
-  // Try JSON-LD first
-  const jldMatches = html.match(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const block of jldMatches) {
-    const inner = block.replace(/<script[^>]*>/, '').replace(/<\/script>/, '');
-    try {
-      const data = JSON.parse(inner);
-      const offers = Array.isArray(data) ? data.flatMap(d => d.offers || []) : (data.offers || []);
-      const arr = Array.isArray(offers) ? offers : [offers];
-      for (const o of arr) {
-        const price = parseFloat(o.price || o.lowPrice || 0);
-        if (price > 0) {
-          // Check if unit is sq ft or m2
-          const unit = (o.unitCode || o.description || JSON.stringify(o)).toLowerCase();
-          if (unit.includes('sqft') || unit.includes('sq ft') || unit.includes('square foot') || unit.includes('square feet') || unit.includes('sq_ft')) {
-            return price * 10.7639; // convert to m2
-          }
-          // assume m2 if price is in range 50-200
-          if (price >= 40 && price <= 250) return price;
-        }
-      }
-    } catch {}
-  }
-
-  // Pattern: "From $X / sq ft" or "From $X/sq ft"
-  const sqftMatch = html.match(/[Ff]rom\s+\$?([\d,.]+)\s*\/\s*sq\.?\s*ft/i);
-  if (sqftMatch) {
-    const val = parseFloat(sqftMatch[1].replace(/,/g, ''));
-    if (val > 0) return val * 10.7639;
-  }
-
-  // Pattern: "From $X / m²" or "From $X/m2"
-  const m2Match = html.match(/[Ff]rom\s+\$?([\d,.]+)\s*\/\s*m[²2]/i);
-  if (m2Match) {
-    const val = parseFloat(m2Match[1].replace(/,/g, ''));
-    if (val > 0) return val;
-  }
-
-  // Look for price chips: "$5.40" near "sq ft"
-  const priceChips = [...html.matchAll(/\$([\d]+\.[\d]{2})/g)];
-  for (const m of priceChips) {
-    const val = parseFloat(m[1]);
-    // sq ft range: $4-$12; m2 range: $43-$130
-    if (val >= 4 && val <= 15) {
-      // check nearby context for sq ft
-      const idx = m.index;
-      const context = html.slice(Math.max(0, idx - 100), idx + 100).toLowerCase();
-      if (context.includes('sq ft') || context.includes('sqft') || context.includes('square')) {
-        return val * 10.7639;
-      }
-    }
-    if (val >= 40 && val <= 150) {
-      const idx = m.index;
-      const context = html.slice(Math.max(0, idx - 100), idx + 100).toLowerCase();
-      if (context.includes('/m') || context.includes('m²') || context.includes('per m')) {
-        return val;
-      }
-    }
-  }
-
-  // Try wider patterns: any price-like value in "price" context
-  // RW typically shows "5.40" near "sq ft" in their product page
-  const genericSqft = html.match(/(\d+\.\d+)\s*(?:USD\s*)?\/\s*sq\.?\s*ft/i);
-  if (genericSqft) {
-    const val = parseFloat(genericSqft[1]);
-    if (val > 0) return val * 10.7639;
-  }
-
-  return null;
-}
-
-// Bucket by per-m2 retail rate
-// C2 ~58.10, C3 ~76.40, C4 ~88.30
-function bucketGroup(ratePerM2) {
-  // Breakpoints: <67 => C2, 67-82 => C3, >82 => C4
-  if (ratePerM2 < 67) return 'C2';
-  if (ratePerM2 <= 82) return 'C3';
-  return 'C4';
-}
-
-const MF_MUTATION = 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message field } } }';
-
-async function processProduct(gid) {
-  const shortId = gid.replace('gid://shopify/Product/', '');
-
-  // Step 1: Fetch rw_product_url and current price_group
-  const fetchQ = {
-    query: `{ product(id:"${gid}") {
-      title
-      metafield_url: metafield(namespace:"custom", key:"rw_product_url") { value }
-      metafield_pg: metafield(namespace:"custom", key:"price_group") { value }
-    } }`
-  };
-  const fr = await gql(fetchQ);
-  const prod = fr?.data?.product;
-  if (!prod) {
-    console.log(`  [${shortId}] SKIP — not found on Shopify`);
-    return { status: 'failed', group: null };
-  }
-
-  const rwUrl = prod.metafield_url?.value || null;
-  const existingPg = prod.metafield_pg?.value || null;
-
-  if (!rwUrl) {
-    console.log(`  [${shortId}] SKIP — no rw_product_url`);
-    return { status: 'unknown', group: null };
-  }
-
-  console.log(`  [${shortId}] "${prod.title?.slice(0,50)}" url=${rwUrl.slice(0,60)} existing_pg=${existingPg || 'none'}`);
-
-  // Step 2: Scrape RW page
-  await sleep(500); // be polite
-  let pricePerM2 = null;
-  try {
-    const res = await fetchUrl(rwUrl);
-    if (res.status === 200) {
-      pricePerM2 = extractPricePerM2(res.body);
-    } else {
-      console.log(`    HTTP ${res.status} for ${rwUrl}`);
-    }
-  } catch (e) {
-    console.log(`    fetch error: ${e.message}`);
-  }
-
-  if (pricePerM2 == null) {
-    console.log(`    Could not extract price — leaving as-is (C3 default)`);
-    return { status: 'unknown', group: existingPg || 'C3' };
-  }
-
-  const group = bucketGroup(pricePerM2);
-  console.log(`    price/m2=$${pricePerM2.toFixed(2)} => ${group} (existing: ${existingPg || 'none'})`);
-
-  // Step 3: Write metafields
-  const metafields = [
-    { ownerId: gid, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-    { ownerId: gid, namespace: 'custom', key: 'cost_basis', value: `${group}: portal RRP-derived, P&S/Commercial extrapolated`, type: 'single_line_text_field' },
-  ];
-
-  // Only overwrite cost metafields for C2 and C4 (C3 stays as-is)
-  if (group !== 'C3') {
-    const costPerM2 = group === 'C2' ? 33.84 : 51.44;
-    metafields.push(
-      { ownerId: gid, namespace: 'custom', key: 'cost_per_m2', value: String(costPerM2), type: 'number_decimal' },
-      { ownerId: gid, namespace: 'custom', key: 'material_options', value: JSON.stringify(MATERIAL_OPTIONS[group]), type: 'json' }
-    );
-  }
-
-  try {
-    const wr = await gql({ query: MF_MUTATION, variables: { m: metafields } });
-    const errs = wr?.data?.metafieldsSet?.userErrors || [];
-    if (errs.length) {
-      console.log(`    WRITE ERRORS: ${errs.map(e => e.message).join('; ')}`);
-      return { status: 'failed', group };
-    }
-    console.log(`    wrote ${metafields.length} metafields for ${group}`);
-    return { status: 'ok', group };
-  } catch (e) {
-    console.log(`    write exception: ${e.message}`);
-    return { status: 'failed', group };
-  }
-}
-
-async function main() {
-  const counts = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0 };
-  const notes = [];
-
-  for (const gid of PRODUCT_IDS) {
-    try {
-      const result = await processProduct(gid);
-      counts.processed++;
-      if (result.status === 'ok') {
-        if (result.group === 'C2') counts.c2++;
-        else if (result.group === 'C3') counts.c3++;
-        else if (result.group === 'C4') counts.c4++;
-      } else if (result.status === 'unknown') {
-        counts.unknown++;
-      } else if (result.status === 'failed') {
-        counts.failed++;
-      } else if (result.status === 'skip_no_url') {
-        counts.unknown++;
-      }
-    } catch (e) {
-      console.log(`  [${gid}] EXCEPTION: ${e.message}`);
-      counts.failed++;
-      counts.processed++;
-    }
-    await sleep(500); // ~2 req/s
-  }
-
-  console.log('\n=== FINAL COUNTS ===');
-  console.log(JSON.stringify(counts, null, 2));
-  return counts;
-}
-
-main().catch(e => { console.error('Fatal:', e); process.exit(1); });
diff --git a/scripts/rw-price-group-fix.js b/scripts/rw-price-group-fix.js
deleted file mode 100644
index aec33fa..0000000
--- a/scripts/rw-price-group-fix.js
+++ /dev/null
@@ -1,398 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-fix.js
- * Determine Rebel Walls price group (C2/C3/C4) from live per-m2 retail rate,
- * then write price_group + cost metafields to the sandbox Shopify store.
- *
- * Usage: node rw-price-group-fix.js
- */
-
-const https = require('https');
-
-const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-// Shopify IDs from the DB slice
-const SHOPIFY_IDS = [
-  'gid://shopify/Product/7855823093811',
-  'gid://shopify/Product/7855823126579',
-  'gid://shopify/Product/7855823159347',
-  'gid://shopify/Product/7855823192115',
-  'gid://shopify/Product/7855823224883',
-  'gid://shopify/Product/7855823257651',
-  'gid://shopify/Product/7855823290419',
-  'gid://shopify/Product/7855823323187',
-  'gid://shopify/Product/7855823355955',
-  'gid://shopify/Product/7855823388723',
-  'gid://shopify/Product/7855823421491',
-  'gid://shopify/Product/7855823454259',
-  'gid://shopify/Product/7855823487027',
-  'gid://shopify/Product/7855823552563',
-  'gid://shopify/Product/7855823618099',
-  'gid://shopify/Product/7855823650867',
-  'gid://shopify/Product/7855823683635',
-  'gid://shopify/Product/7855823716403',
-  'gid://shopify/Product/7855823749171',
-  'gid://shopify/Product/7855823781939',
-  'gid://shopify/Product/7855823585331',
-  'gid://shopify/Product/7855823814707',
-  'gid://shopify/Product/7855823847475',
-  'gid://shopify/Product/7855823880243',
-  'gid://shopify/Product/7855823913011',
-  'gid://shopify/Product/7855823945779',
-  'gid://shopify/Product/7855823978547',
-  'gid://shopify/Product/7855824011315',
-  'gid://shopify/Product/7855824044083',
-  'gid://shopify/Product/7855824076851',
-  'gid://shopify/Product/7855824109619',
-  'gid://shopify/Product/7855824175155',
-  'gid://shopify/Product/7855824207923',
-  'gid://shopify/Product/7855824240691',
-  'gid://shopify/Product/7855824273459',
-  'gid://shopify/Product/7855824306227',
-  'gid://shopify/Product/7855824338995',
-  'gid://shopify/Product/7855824371763',
-  'gid://shopify/Product/7855824404531',
-  'gid://shopify/Product/7855824470067',
-];
-
-// Pre-computed material_options per group
-const MATERIAL_OPTIONS = {
-  C2: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-  ]),
-  C3: null, // leave as-is
-  C4: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-  ]),
-};
-
-const COST_PER_M2 = { C2: '33.84', C3: '44.50', C4: '51.44' };
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gql(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ _raw: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data);
-    req.end();
-  });
-}
-
-function fetchUrl(url) {
-  return new Promise((resolve, reject) => {
-    const parsed = new URL(url);
-    const mod = parsed.protocol === 'https:' ? https : require('http');
-    const options = {
-      hostname: parsed.hostname,
-      path: parsed.pathname + parsed.search,
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.5',
-      }
-    };
-    const req = mod.request(options, res => {
-      // Handle redirects
-      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-        resolve(fetchUrl(res.headers.location));
-        return;
-      }
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => resolve({ status: res.statusCode, body: c }));
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.end();
-  });
-}
-
-/**
- * Extract per-m2 price from Rebel Walls product page HTML.
- * RW shows prices in USD per sq ft or per m2 depending on locale.
- * We look for JSON-LD and also the price chip patterns.
- */
-function extractPricePerM2(html) {
-  // Try JSON-LD first
-  const jsonLdMatches = [...html.matchAll(/<script[^>]+type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi)];
-  for (const m of jsonLdMatches) {
-    try {
-      const obj = JSON.parse(m[1]);
-      const offers = obj.offers || (Array.isArray(obj) ? obj.flatMap(x => x.offers || []) : []);
-      const offerArr = Array.isArray(offers) ? offers : [offers];
-      for (const o of offerArr) {
-        if (o && o.price) {
-          const price = parseFloat(o.price);
-          if (price > 0) {
-            const currency = (o.priceCurrency || '').toUpperCase();
-            const unitText = JSON.stringify(o).toLowerCase();
-            // If it's per sq ft (small number, typically 3-15 USD), convert to m2
-            if (price < 20 && (unitText.includes('sq') || unitText.includes('sqft') || unitText.includes('sq_ft') || unitText.includes('square'))) {
-              return { pricePerM2: price * 10.7639, source: 'json-ld-sqft' };
-            }
-            // If it's per m2 (typically 40-150), use directly
-            if (price >= 30 && price <= 200) {
-              return { pricePerM2: price, source: 'json-ld-m2' };
-            }
-          }
-        }
-      }
-    } catch {}
-  }
-
-  // Try price patterns in HTML
-  // Look for "From $X / sq ft" or "From $X/sqft"
-  const sqftPatterns = [
-    /\$\s*([\d.]+)\s*\/\s*sq\s*ft/i,
-    /\$\s*([\d.]+)\s*\/\s*sqft/i,
-    /\$\s*([\d.]+)\s*per\s*sq\s*ft/i,
-    /"price":\s*"?([\d.]+)"?[^}]*"sq/i,
-    /From\s+\$\s*([\d.]+)\s*\/\s*sq/i,
-  ];
-  for (const pat of sqftPatterns) {
-    const m = html.match(pat);
-    if (m) {
-      const price = parseFloat(m[1]);
-      if (price > 0 && price < 30) {
-        return { pricePerM2: price * 10.7639, source: 'html-sqft' };
-      }
-    }
-  }
-
-  // Look for per-m2 patterns
-  const m2Patterns = [
-    /\$\s*([\d.]+)\s*\/\s*m[²2]/i,
-    /\$\s*([\d.]+)\s*per\s*m[²2]/i,
-    /From\s+\$\s*([\d.]+)\s*\/\s*m/i,
-    /"price":\s*"?([\d.]+)"?[^}]*"m2/i,
-    /"price":\s*"?([\d.]+)"?[^}]*\/m²/i,
-  ];
-  for (const pat of m2Patterns) {
-    const m = html.match(pat);
-    if (m) {
-      const price = parseFloat(m[1]);
-      if (price >= 30 && price <= 250) {
-        return { pricePerM2: price, source: 'html-m2' };
-      }
-    }
-  }
-
-  // Look for data-price attributes or price in JS
-  const dataPatterns = [
-    /data-price[^>]*>([\d.]+)</i,
-    /"base_price":\s*([\d.]+)/i,
-    /"basePrice":\s*([\d.]+)/i,
-    /pricePerSqFt[^:]*:\s*([\d.]+)/i,
-    /price_per_sqft[^:]*:\s*([\d.]+)/i,
-  ];
-  for (const pat of dataPatterns) {
-    const m = html.match(pat);
-    if (m) {
-      const price = parseFloat(m[1]);
-      if (price > 0 && price < 30) {
-        return { pricePerM2: price * 10.7639, source: 'html-data-sqft' };
-      }
-      if (price >= 30 && price <= 250) {
-        return { pricePerM2: price, source: 'html-data-m2' };
-      }
-    }
-  }
-
-  return null;
-}
-
-/**
- * Bucket a per-m2 price into C2/C3/C4.
- * C2 ~58.10, C3 ~76.40, C4 ~88.30
- * Pick nearest bucket.
- */
-function bucketPrice(pricePerM2) {
-  const buckets = [
-    { group: 'C2', center: 58.10 },
-    { group: 'C3', center: 76.40 },
-    { group: 'C4', center: 88.30 },
-  ];
-  let best = null, bestDist = Infinity;
-  for (const b of buckets) {
-    const dist = Math.abs(pricePerM2 - b.center);
-    if (dist < bestDist) { bestDist = dist; best = b.group; }
-  }
-  return best;
-}
-
-async function fetchProductMetafields(pid) {
-  const r = await gql({
-    query: `{
-      product(id:"${pid}") {
-        rw_product_url: metafield(namespace:"custom", key:"rw_product_url") { value }
-        pg: metafield(namespace:"custom", key:"price_group") { value }
-      }
-    }`
-  });
-  return r?.data?.product;
-}
-
-const MF_MUTATION = `mutation metafieldsSet($m: [MetafieldsSetInput!]!) {
-  metafieldsSet(metafields: $m) {
-    metafields { key value }
-    userErrors { message field }
-  }
-}`;
-
-async function writeMetafields(pid, group) {
-  const mfs = [
-    { ownerId: pid, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-    {
-      ownerId: pid, namespace: 'custom', key: 'cost_basis',
-      value: `${group} — portal-RRP-derived; P&S and Commercial extrapolated`,
-      type: 'single_line_text_field'
-    },
-  ];
-
-  if (group !== 'C3') {
-    mfs.push({
-      ownerId: pid, namespace: 'custom', key: 'cost_per_m2',
-      value: COST_PER_M2[group],
-      type: 'number_decimal'
-    });
-    mfs.push({
-      ownerId: pid, namespace: 'custom', key: 'material_options',
-      value: MATERIAL_OPTIONS[group],
-      type: 'json'
-    });
-  }
-
-  const r = await gql({ query: MF_MUTATION, variables: { m: mfs } });
-  const errs = r?.data?.metafieldsSet?.userErrors || [];
-  return { ok: errs.length === 0, errors: errs };
-}
-
-async function processOne(pid) {
-  // Step 1: fetch metafields
-  let meta;
-  try {
-    meta = await fetchProductMetafields(pid);
-  } catch (e) {
-    return { status: 'failed', reason: `fetch metafields: ${e.message}` };
-  }
-
-  if (!meta) return { status: 'failed', reason: 'product not found' };
-
-  const rwUrl = meta.rw_product_url?.value;
-  if (!rwUrl) return { status: 'skipped', reason: 'no rw_product_url' };
-
-  // Step 2: fetch RW page and extract price
-  let priceResult = null;
-  try {
-    const resp = await fetchUrl(rwUrl);
-    if (resp.status === 200) {
-      priceResult = extractPricePerM2(resp.body);
-    } else {
-      return { status: 'unknown', reason: `HTTP ${resp.status} for ${rwUrl}` };
-    }
-  } catch (e) {
-    return { status: 'unknown', reason: `fetch URL failed: ${e.message}` };
-  }
-
-  let group;
-  let priceNote = '';
-  if (priceResult) {
-    group = bucketPrice(priceResult.pricePerM2);
-    priceNote = `$${priceResult.pricePerM2.toFixed(2)}/m2 (${priceResult.source}) => ${group}`;
-  } else {
-    // Can't determine price — leave as C3 default
-    return { status: 'unknown', reason: `could not extract price from ${rwUrl}` };
-  }
-
-  // Step 3: write metafields
-  let writeResult;
-  try {
-    writeResult = await writeMetafields(pid, group);
-  } catch (e) {
-    return { status: 'failed', reason: `write metafields: ${e.message}` };
-  }
-
-  if (!writeResult.ok) {
-    return { status: 'failed', reason: `userErrors: ${writeResult.errors.map(e => e.message).join('; ')}` };
-  }
-
-  return { status: 'ok', group, priceNote };
-}
-
-async function main() {
-  const counts = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0 };
-  const notes = [];
-
-  console.log(`Processing ${SHOPIFY_IDS.length} products...`);
-
-  for (let i = 0; i < SHOPIFY_IDS.length; i++) {
-    const pid = SHOPIFY_IDS[i];
-    const shortId = pid.replace('gid://shopify/Product/', '');
-    process.stdout.write(`[${i+1}/${SHOPIFY_IDS.length}] ${shortId} ... `);
-
-    let result;
-    try {
-      result = await processOne(pid);
-    } catch (e) {
-      result = { status: 'failed', reason: e.message };
-    }
-
-    counts.processed++;
-
-    if (result.status === 'ok') {
-      const g = result.group.toLowerCase();
-      counts[g]++;
-      console.log(`OK ${result.group} — ${result.priceNote}`);
-    } else if (result.status === 'skipped') {
-      console.log(`SKIP — ${result.reason}`);
-      // skipped = no URL, treat as unknown
-      counts.unknown++;
-      notes.push(`${shortId}: SKIP — ${result.reason}`);
-    } else if (result.status === 'unknown') {
-      counts.unknown++;
-      console.log(`UNKNOWN — ${result.reason}`);
-      notes.push(`${shortId}: UNKNOWN — ${result.reason}`);
-    } else {
-      counts.failed++;
-      console.log(`FAILED — ${result.reason}`);
-      notes.push(`${shortId}: FAILED — ${result.reason}`);
-    }
-
-    // ~2 req/s (500ms between products = 2 Shopify calls + 1 RW fetch per product)
-    if (i < SHOPIFY_IDS.length - 1) await sleep(500);
-  }
-
-  console.log('\n=== RESULTS ===');
-  console.log(JSON.stringify(counts, null, 2));
-  console.log('\nNotes:');
-  notes.forEach(n => console.log(' ', n));
-
-  // Output final JSON for the orchestrator
-  console.log('\n__FINAL__');
-  console.log(JSON.stringify({ ...counts, notes: notes.join(' | ') }));
-}
-
-main().catch(e => { console.error('FATAL:', e); process.exit(1); });
diff --git a/scripts/rw-price-group-setter.js b/scripts/rw-price-group-setter.js
deleted file mode 100644
index 1c2a35e..0000000
--- a/scripts/rw-price-group-setter.js
+++ /dev/null
@@ -1,331 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-setter.js
- * Determine each Rebel Walls mural's PRICE GROUP (C2/C3/C4) from its live
- * per-m2 retail rate on rebelwalls.com, then write price_group + cost metafields.
- *
- * Usage: node rw-price-group-setter.js
- * Reads SHOPIFY_ADMIN_TOKEN from env.
- */
-
-'use strict';
-const https = require('https');
-const http = require('http');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API_PATH = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-// Product IDs from the slice (OFFSET 440 LIMIT 40)
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7851180261427',
-  'gid://shopify/Product/7851180294195',
-  'gid://shopify/Product/7851180326963',
-  'gid://shopify/Product/7851180359731',
-  'gid://shopify/Product/7851180392499',
-  'gid://shopify/Product/7851180458035',
-  'gid://shopify/Product/7851180490803',
-  'gid://shopify/Product/7851180523571',
-  'gid://shopify/Product/7851180556339',
-  'gid://shopify/Product/7851180589107',
-  'gid://shopify/Product/7851180621875',
-  'gid://shopify/Product/7851180654643',
-  'gid://shopify/Product/7851180425267',
-  'gid://shopify/Product/7851180720179',
-  'gid://shopify/Product/7851180752947',
-  'gid://shopify/Product/7851180785715',
-  'gid://shopify/Product/7851180818483',
-  'gid://shopify/Product/7851180851251',
-  'gid://shopify/Product/7851180884019',
-  'gid://shopify/Product/7851180916787',
-  'gid://shopify/Product/7851180949555',
-  'gid://shopify/Product/7851180982323',
-  'gid://shopify/Product/7851181015091',
-  'gid://shopify/Product/7851181047859',
-  'gid://shopify/Product/7851181080627',
-  'gid://shopify/Product/7851181113395',
-  'gid://shopify/Product/7851181146163',
-  'gid://shopify/Product/7851181178931',
-  'gid://shopify/Product/7851181211699',
-  'gid://shopify/Product/7851181244467',
-  'gid://shopify/Product/7851181277235',
-  'gid://shopify/Product/7851181310003',
-  'gid://shopify/Product/7851181375539',
-  'gid://shopify/Product/7851181342771',
-  'gid://shopify/Product/7851181408307',
-  'gid://shopify/Product/7851181441075',
-  'gid://shopify/Product/7851181473843',
-  'gid://shopify/Product/7851181506611',
-  'gid://shopify/Product/7851181539379',
-  'gid://shopify/Product/7851181572147',
-];
-
-// Pre-computed per-group material_options JSON
-const MATERIAL_OPTIONS = {
-  C2: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-  ]),
-  C3: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":76.40,"cost_per_m2":44.50,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":91.70,"cost_per_m2":53.37,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":107.00,"cost_per_m2":62.32,"cost_confirmed":false}
-  ]),
-  C4: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-  ]),
-};
-
-const COST_PER_M2 = { C2: '33.84', C3: '44.50', C4: '51.44' };
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-// ---- Shopify GraphQL ----
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API_PATH, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(60000, () => { req.destroy(); reject(new Error('gql timeout')); });
-    req.write(data);
-    req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const lowBudget = (result?.extensions?.cost?.throttleStatus?.currentlyAvailable || 9999) < 300;
-      if (throttled) { await sleep(3000); continue; }
-      if (lowBudget) await sleep(1500);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-// ---- Fetch RW product page ----
-function fetchUrl(url) {
-  return new Promise((resolve, reject) => {
-    const mod = url.startsWith('https') ? https : http;
-    const req = mod.get(url, {
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.5',
-      }
-    }, res => {
-      // Handle redirect
-      if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
-        const loc = res.headers.location;
-        if (loc) {
-          const newUrl = loc.startsWith('http') ? loc : `https://rebelwalls.com${loc}`;
-          return fetchUrl(newUrl).then(resolve).catch(reject);
-        }
-      }
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => resolve({ body: c, status: res.statusCode }));
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('fetch timeout: ' + url)); });
-  });
-}
-
-// ---- Extract base price per m2 from RW page ----
-function extractBasePriceM2(html) {
-  // Best: "minimum_product_price":"$XX.XX" from the embedded JSON data
-  // This is the base material (Non-Woven) minimum per-m2 price
-  const m = html.match(/"minimum_product_price":"?\$?([0-9]+\.?[0-9]*)"?/);
-  if (m) {
-    const val = parseFloat(m[1]);
-    if (!isNaN(val) && val > 0) return val;
-  }
-
-  // Fallback: productMaterials std type minimumProductPrice
-  const matsM = html.match(/"productMaterials":\s*(\[[\s\S]*?\])/);
-  if (matsM) {
-    try {
-      const mats = JSON.parse(matsM[1]);
-      const std = mats.find(m => m.type === 'std');
-      if (std) {
-        const p = String(std.minimumProductPrice || '').replace(/[^0-9.]/g, '');
-        const v = parseFloat(p);
-        if (!isNaN(v) && v > 0) return v;
-        // Try defaultPrice and convert sq ft -> m2
-        const dp = String(std.defaultPrice || '').replace(/[^0-9.]/g, '');
-        const dv = parseFloat(dp);
-        if (!isNaN(dv) && dv > 0) return dv * 10.7639;
-      }
-    } catch {}
-  }
-
-  // Last resort: find the smallest dollar amount that's in range 50-100
-  const allPrices = [...html.matchAll(/\$([0-9]+\.[0-9]+)/g)]
-    .map(m => parseFloat(m[1]))
-    .filter(v => v >= 50 && v <= 120);
-  if (allPrices.length > 0) return Math.min(...allPrices);
-
-  return null;
-}
-
-// ---- Bucket price to group ----
-function priceGroup(priceM2) {
-  // C2 ~58.10, C3 ~76.40, C4 ~88.30
-  // Midpoints: (58.10+76.40)/2 = 67.25,  (76.40+88.30)/2 = 82.35
-  if (priceM2 <= 67.25) return 'C2';
-  if (priceM2 <= 82.35) return 'C3';
-  return 'C4';
-}
-
-// ---- Write metafields ----
-const MF_MUTATION = 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message field } } }';
-
-async function writeMetafields(ownerId, group) {
-  const mfs = [
-    { ownerId, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-  ];
-
-  // Always write cost_basis note
-  mfs.push({
-    ownerId, namespace: 'custom', key: 'cost_basis',
-    value: `${group} portal-RRP-derived; P&S/Commercial extrapolated`,
-    type: 'single_line_text_field'
-  });
-
-  if (group !== 'C3') {
-    // Overwrite cost_per_m2 and material_options for C2/C4
-    mfs.push({
-      ownerId, namespace: 'custom', key: 'cost_per_m2',
-      value: COST_PER_M2[group],
-      type: 'number_decimal'
-    });
-    mfs.push({
-      ownerId, namespace: 'custom', key: 'material_options',
-      value: MATERIAL_OPTIONS[group],
-      type: 'json'
-    });
-  }
-
-  const r = await gql({ query: MF_MUTATION, variables: { m: mfs } });
-  const errs = r?.data?.metafieldsSet?.userErrors || [];
-  if (errs.length) {
-    throw new Error(`metafieldsSet errors: ${errs.map(e => e.message).join('; ')}`);
-  }
-  return mfs.length;
-}
-
-// ---- Main ----
-async function main() {
-  const stats = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0, notes: [] };
-
-  console.log(`Processing ${PRODUCT_IDS.length} products...`);
-
-  for (let i = 0; i < PRODUCT_IDS.length; i++) {
-    const pid = PRODUCT_IDS[i];
-    const shortId = pid.replace('gid://shopify/Product/', '');
-
-    try {
-      // 1. Fetch rw_product_url + existing price_group
-      const fetchR = await gql({
-        query: `{ product(id:"${pid}") {
-          title
-          metafield(namespace:"custom",key:"rw_product_url"){ value }
-          pg:metafield(namespace:"custom",key:"price_group"){ value }
-        }}`
-      });
-      const prod = fetchR?.data?.product;
-      if (!prod) {
-        console.log(`[${i+1}/${PRODUCT_IDS.length}] ${shortId} — SKIP: not found on Shopify`);
-        stats.failed++;
-        continue;
-      }
-      const rwUrl = prod.metafield?.value;
-      const existingGroup = prod.pg?.value;
-      const title = prod.title || '(no title)';
-
-      if (!rwUrl) {
-        console.log(`[${i+1}/${PRODUCT_IDS.length}] ${shortId} "${title}" — SKIP: no rw_product_url`);
-        stats.unknown++;
-        continue;
-      }
-
-      // 2. Fetch the RW page and extract base price
-      await sleep(400); // rate limit for RW
-      let priceM2 = null;
-      let group = 'C3'; // default
-      let groupSource = 'default';
-
-      try {
-        const { body, status } = await fetchUrl(rwUrl);
-        if (status === 200) {
-          priceM2 = extractBasePriceM2(body);
-          if (priceM2 !== null) {
-            group = priceGroup(priceM2);
-            groupSource = `m2=$${priceM2.toFixed(2)}`;
-          } else {
-            console.log(`  WARNING: could not extract price from ${rwUrl}`);
-            stats.notes.push(`${shortId}: no price extracted from ${rwUrl}`);
-            stats.unknown++;
-            stats.processed++;
-            continue;
-          }
-        } else {
-          console.log(`  WARNING: ${rwUrl} returned HTTP ${status}`);
-          stats.notes.push(`${shortId}: HTTP ${status} from ${rwUrl}`);
-          stats.unknown++;
-          stats.processed++;
-          continue;
-        }
-      } catch (fetchErr) {
-        console.log(`  WARNING: fetch failed for ${rwUrl}: ${fetchErr.message}`);
-        stats.notes.push(`${shortId}: fetch error: ${fetchErr.message}`);
-        stats.unknown++;
-        stats.processed++;
-        continue;
-      }
-
-      // 3. Write metafields
-      await sleep(400);
-      const mfCount = await writeMetafields(pid, group);
-
-      stats.processed++;
-      stats[group.toLowerCase()]++;
-      console.log(`[${i+1}/${PRODUCT_IDS.length}] ${shortId} "${title}" — ${group} (${groupSource}, was:${existingGroup||'none'}) — wrote ${mfCount} metafields`);
-
-    } catch (err) {
-      console.error(`[${i+1}/${PRODUCT_IDS.length}] ${shortId} — FAILED: ${err.message}`);
-      stats.failed++;
-      stats.notes.push(`${shortId}: ${err.message}`);
-    }
-
-    // Rate limiting: ~2 req/s (Shopify) + RW page fetches
-    await sleep(500);
-  }
-
-  console.log('\n=== RESULTS ===');
-  console.log(JSON.stringify(stats, null, 2));
-  return stats;
-}
-
-main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
diff --git a/scripts/rw-price-group-slice7.js b/scripts/rw-price-group-slice7.js
deleted file mode 100644
index ed5696c..0000000
--- a/scripts/rw-price-group-slice7.js
+++ /dev/null
@@ -1,318 +0,0 @@
-#!/usr/bin/env node
-/**
- * rw-price-group-slice7.js
- * Process OFFSET 280, LIMIT 40 of Rebel Walls products.
- * For each: fetch rw_product_url metafield, curl the RW page, extract base price,
- * bucket into C2/C3/C4, write price_group + cost metafields to sandbox Shopify.
- */
-
-'use strict';
-const https = require('https');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-// Pre-computed per-group material_options
-const MATERIAL_OPTIONS = {
-  C2: JSON.stringify([
-    { material: 'Non-Woven (Standard)', retail_per_m2: 58.10, cost_per_m2: 33.84, cost_confirmed: false },
-    { material: 'Peel & Stick', retail_per_m2: 69.70, cost_per_m2: 40.60, cost_confirmed: false },
-    { material: 'Commercial Grade', retail_per_m2: 81.36, cost_per_m2: 47.39, cost_confirmed: false }
-  ]),
-  C3: JSON.stringify([
-    { material: 'Non-Woven (Standard)', retail_per_m2: 76.40, cost_per_m2: 44.50, cost_confirmed: false },
-    { material: 'Peel & Stick', retail_per_m2: 91.68, cost_per_m2: 53.40, cost_confirmed: false },
-    { material: 'Commercial Grade', retail_per_m2: 107.00, cost_per_m2: 62.33, cost_confirmed: false }
-  ]),
-  C4: JSON.stringify([
-    { material: 'Non-Woven (Standard)', retail_per_m2: 88.30, cost_per_m2: 51.44, cost_confirmed: false },
-    { material: 'Peel & Stick', retail_per_m2: 106.00, cost_per_m2: 61.75, cost_confirmed: false },
-    { material: 'Commercial Grade', retail_per_m2: 123.66, cost_per_m2: 72.03, cost_confirmed: false }
-  ])
-};
-
-const COST_PER_M2 = { C2: '33.84', C3: '44.50', C4: '51.44' };
-
-// Buckets: base per-m2 retail
-function bucketPrice(price) {
-  // C2 ~58.10, C3 ~76.40, C4 ~88.30
-  // Midpoints: C2/C3 = (58.10+76.40)/2 = 67.25, C3/C4 = (76.40+88.30)/2 = 82.35
-  if (price < 67.25) return 'C2';
-  if (price < 82.35) return 'C3';
-  return 'C4';
-}
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gqlRaw(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data)
-      }
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); } });
-    });
-    req.on('error', reject);
-    req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data); req.end();
-  });
-}
-
-async function gql(body, retries = 4) {
-  for (let attempt = 1; attempt <= retries; attempt++) {
-    try {
-      const result = await gqlRaw(body);
-      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
-      const avail = result?.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 9999;
-      if (throttled) { await sleep(3000); continue; }
-      if (avail < 300) await sleep(1500);
-      return result;
-    } catch (e) {
-      if (attempt < retries) { await sleep(attempt * 2000); continue; }
-      throw e;
-    }
-  }
-}
-
-function fetchUrl(urlStr) {
-  return new Promise((resolve, reject) => {
-    const url = new URL(urlStr);
-    const options = {
-      hostname: url.hostname,
-      path: url.pathname + (url.search || ''),
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.9'
-      }
-    };
-    const mod = require(url.protocol === 'https:' ? 'https' : 'http');
-    let redirectCount = 0;
-    function doRequest(opts) {
-      const req = mod.request(opts, res => {
-        if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location && redirectCount < 5) {
-          redirectCount++;
-          const redir = new URL(res.headers.location, `${opts.hostname}`);
-          const newMod = require(redir.protocol === 'https:' ? 'https' : 'http');
-          const newOpts = { ...opts, hostname: redir.hostname, path: redir.pathname + (redir.search || '') };
-          res.resume();
-          doRequest(newOpts);
-          return;
-        }
-        let body = '';
-        res.on('data', d => body += d);
-        res.on('end', () => resolve(body));
-      });
-      req.on('error', reject);
-      req.setTimeout(30000, () => { req.destroy(); reject(new Error('fetch timeout')); });
-      req.end();
-    }
-    doRequest(options);
-  });
-}
-
-function extractPrice(html) {
-  // Primary: JSON-LD Product with priceCurrency USD
-  const ldMatches = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const tag of ldMatches) {
-    const inner = tag.replace(/<script[^>]*>/, '').replace(/<\/script>/, '').trim();
-    try {
-      const obj = JSON.parse(inner);
-      if (obj['@type'] === 'Product' && obj.offers) {
-        const offer = Array.isArray(obj.offers) ? obj.offers[0] : obj.offers;
-        if (offer && offer.priceCurrency === 'USD' && offer.price) {
-          const p = parseFloat(offer.price);
-          if (!isNaN(p) && p > 0) return p;
-        }
-      }
-    } catch { /* ignore */ }
-  }
-
-  // Fallback: data-price attribute or "price":"XX.XX" near USD
-  const priceUsd = html.match(/"price"\s*:\s*"?([\d.]+)"?\s*[^}]*"priceCurrency"\s*:\s*"USD"/);
-  if (priceUsd) {
-    const p = parseFloat(priceUsd[1]);
-    if (!isNaN(p) && p > 0) return p;
-  }
-
-  // Second fallback: look for price chip text "$X.XX / sq ft" -> convert to m2
-  const sqft = html.match(/\$\s*([\d.]+)\s*\/\s*sq\.?\s*ft/i);
-  if (sqft) {
-    const perSqft = parseFloat(sqft[1]);
-    if (!isNaN(perSqft) && perSqft > 0) return perSqft * 10.7639;
-  }
-
-  return null;
-}
-
-async function writeMetafields(pid, group) {
-  const mfs = [
-    { ownerId: pid, namespace: 'custom', key: 'price_group', value: group, type: 'single_line_text_field' },
-    { ownerId: pid, namespace: 'custom', key: 'material_options', value: MATERIAL_OPTIONS[group], type: 'json' },
-    { ownerId: pid, namespace: 'custom', key: 'cost_basis', value: `${group} — portal-RRP-derived; P&S/Commercial extrapolated`, type: 'single_line_text_field' }
-  ];
-
-  // Only overwrite cost_per_m2 if not C3
-  if (group !== 'C3') {
-    mfs.push({ ownerId: pid, namespace: 'custom', key: 'cost_per_m2', value: COST_PER_M2[group], type: 'number_decimal' });
-  }
-
-  const r = await gql({
-    query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key value } userErrors { message field } } }',
-    variables: { m: mfs }
-  });
-  const errs = r?.data?.metafieldsSet?.userErrors || [];
-  if (errs.length) throw new Error(errs.map(e => e.message).join('; '));
-  return true;
-}
-
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7851174166579',
-  'gid://shopify/Product/7851174199347',
-  'gid://shopify/Product/7851174232115',
-  'gid://shopify/Product/7851174264883',
-  'gid://shopify/Product/7851174297651',
-  'gid://shopify/Product/7851174330419',
-  'gid://shopify/Product/7851174363187',
-  'gid://shopify/Product/7851174395955',
-  'gid://shopify/Product/7851174428723',
-  'gid://shopify/Product/7851174461491',
-  'gid://shopify/Product/7851174494259',
-  'gid://shopify/Product/7851174527027',
-  'gid://shopify/Product/7851174559795',
-  'gid://shopify/Product/7851174592563',
-  'gid://shopify/Product/7851174625331',
-  'gid://shopify/Product/7851174658099',
-  'gid://shopify/Product/7851174690867',
-  'gid://shopify/Product/7851174723635',
-  'gid://shopify/Product/7851174756403',
-  'gid://shopify/Product/7851174789171',
-  'gid://shopify/Product/7851174821939',
-  'gid://shopify/Product/7851174854707',
-  'gid://shopify/Product/7851174887475',
-  'gid://shopify/Product/7851174985779',
-  'gid://shopify/Product/7851175084083',
-  'gid://shopify/Product/7851175182387',
-  'gid://shopify/Product/7851175313459',
-  'gid://shopify/Product/7851175346227',
-  'gid://shopify/Product/7851175411763',
-  'gid://shopify/Product/7851175444531',
-  'gid://shopify/Product/7851175477299',
-  'gid://shopify/Product/7851175542835',
-  'gid://shopify/Product/7851175608371',
-  'gid://shopify/Product/7851175673907',
-  'gid://shopify/Product/7851175804979',
-  'gid://shopify/Product/7851175837747',
-  'gid://shopify/Product/7851175903283',
-  'gid://shopify/Product/7851175510067',
-  'gid://shopify/Product/7851176034355',
-  'gid://shopify/Product/7851176067123'
-];
-
-async function main() {
-  const counts = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0 };
-  const notes = [];
-
-  for (const pid of PRODUCT_IDS) {
-    try {
-      // 1. Fetch metafields
-      const r = await gql({
-        query: `{ product(id:"${pid}") {
-          title
-          metafield(namespace:"custom",key:"rw_product_url"){ value }
-          pg: metafield(namespace:"custom",key:"price_group"){ value }
-        }}`
-      });
-      const prod = r?.data?.product;
-      if (!prod) {
-        console.log(`SKIP ${pid}: product not found`);
-        counts.unknown++;
-        counts.processed++;
-        continue;
-      }
-
-      const rwUrl = prod.metafield?.value;
-      const existingPg = prod.pg?.value;
-      const title = prod.title || '';
-
-      if (!rwUrl) {
-        console.log(`SKIP ${pid} ("${title}"): no rw_product_url`);
-        counts.unknown++;
-        counts.processed++;
-        continue;
-      }
-
-      // 2. Fetch product page
-      await sleep(500); // polite delay
-      let html;
-      try {
-        html = await fetchUrl(rwUrl);
-      } catch (e) {
-        console.log(`FAIL ${pid} ("${title}"): fetch error: ${e.message}`);
-        counts.failed++;
-        counts.processed++;
-        notes.push(`fetch-fail: ${pid} ${e.message}`);
-        continue;
-      }
-
-      // 3. Extract price
-      const price = extractPrice(html);
-      if (price === null) {
-        console.log(`UNKNOWN ${pid} ("${title}"): could not extract price from ${rwUrl}`);
-        counts.unknown++;
-        counts.processed++;
-        notes.push(`no-price: ${pid} ${rwUrl}`);
-        continue;
-      }
-
-      const group = bucketPrice(price);
-      console.log(`${pid} | "${title}" | price=${price.toFixed(2)}/m2 => ${group} (was: ${existingPg || 'unset'})`);
-
-      // 4. Write metafields
-      try {
-        await writeMetafields(pid, group);
-        console.log(`  -> wrote price_group=${group}, cost_per_m2=${COST_PER_M2[group]}, material_options, cost_basis`);
-        counts[group.toLowerCase()]++;
-        counts.processed++;
-      } catch (e) {
-        console.log(`  WRITE FAIL ${pid}: ${e.message}`);
-        counts.failed++;
-        counts.processed++;
-        notes.push(`write-fail: ${pid} ${e.message}`);
-      }
-
-      await sleep(500); // ~2 req/s
-    } catch (e) {
-      console.log(`EXCEPTION ${pid}: ${e.message}`);
-      counts.failed++;
-      counts.processed++;
-      notes.push(`exception: ${pid} ${e.message}`);
-    }
-  }
-
-  const result = {
-    processed: counts.processed,
-    c2: counts.c2,
-    c3: counts.c3,
-    c4: counts.c4,
-    unknown: counts.unknown,
-    failed: counts.failed,
-    notes: notes.join(' | ') || 'none'
-  };
-  console.log('\n=== FINAL RESULT ===');
-  console.log(JSON.stringify(result, null, 2));
-  return result;
-}
-
-main().catch(e => { console.error('FATAL:', e.message, e.stack); process.exit(1); });
diff --git a/tmp-rw-price-group.js b/tmp-rw-price-group.js
deleted file mode 100644
index 38e12a1..0000000
--- a/tmp-rw-price-group.js
+++ /dev/null
@@ -1,337 +0,0 @@
-#!/usr/bin/env node
-/**
- * Rebel Walls price-group bucketer — SANDBOX
- * Fetches rw_product_url from each product, curls the RW page,
- * extracts base per-m2 rate, buckets C2/C3/C4, writes metafields.
- */
-
-const https = require('https');
-const http = require('http');
-
-const STORE = 'designer-laboratory-sandbox.myshopify.com';
-const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const API = '/admin/api/2024-10/graphql.json';
-
-if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }
-
-const PRODUCT_IDS = [
-  'gid://shopify/Product/7855838789683',
-  'gid://shopify/Product/7855838822451',
-  'gid://shopify/Product/7855838887987',
-  'gid://shopify/Product/7855838920755',
-  'gid://shopify/Product/7855838986291',
-  'gid://shopify/Product/7855839019059',
-  'gid://shopify/Product/7855839084595',
-  'gid://shopify/Product/7855839117363',
-  'gid://shopify/Product/7855839150131',
-  'gid://shopify/Product/7855839182899',
-  'gid://shopify/Product/7855839215667',
-  'gid://shopify/Product/7855839248435',
-  'gid://shopify/Product/7855839313971',
-  'gid://shopify/Product/7855839379507',
-  'gid://shopify/Product/7855839412275',
-  'gid://shopify/Product/7855839477811',
-  'gid://shopify/Product/7855839510579',
-  'gid://shopify/Product/7855839543347',
-  'gid://shopify/Product/7855839576115',
-  'gid://shopify/Product/7855839739955',
-  'gid://shopify/Product/7855839805491',
-  'gid://shopify/Product/7855839838259',
-  'gid://shopify/Product/7855839608883',
-  'gid://shopify/Product/7855839707187',
-  'gid://shopify/Product/7855839936563',
-  'gid://shopify/Product/7855839969331',
-  'gid://shopify/Product/7855840002099',
-  'gid://shopify/Product/7855840034867',
-  'gid://shopify/Product/7855840067635',
-  'gid://shopify/Product/7855840100403',
-  'gid://shopify/Product/7855840165939',
-  'gid://shopify/Product/7855840329779',
-  'gid://shopify/Product/7855840362547',
-  'gid://shopify/Product/7855840395315',
-  'gid://shopify/Product/7855840460851',
-  'gid://shopify/Product/7855840493619',
-  'gid://shopify/Product/7855840526387',
-  'gid://shopify/Product/7855840559155',
-  'gid://shopify/Product/7855840624691',
-  'gid://shopify/Product/7855840657459',
-];
-
-// Price group buckets (per m2 USD)
-const BUCKETS = [
-  { group: 'C2', center: 58.10 },
-  { group: 'C3', center: 76.40 },
-  { group: 'C4', center: 88.30 },
-];
-
-const MATERIAL_OPTIONS = {
-  C2: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
-  ]),
-  C3: null,
-  C4: JSON.stringify([
-    {"material":"Non-Woven (Standard)","retail_per_m2":88.30,"cost_per_m2":51.44,"cost_confirmed":false},
-    {"material":"Peel & Stick","retail_per_m2":106.00,"cost_per_m2":61.75,"cost_confirmed":false},
-    {"material":"Commercial Grade","retail_per_m2":123.66,"cost_per_m2":72.03,"cost_confirmed":false}
-  ]),
-};
-
-const COST_PER_M2 = { C2: '33.84', C3: '44.5', C4: '51.44' };
-
-function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
-
-function gql(body) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify(body);
-    const req = https.request({
-      hostname: STORE, path: API, method: 'POST',
-      headers: {
-        'X-Shopify-Access-Token': TOKEN,
-        'Content-Type': 'application/json',
-        'Content-Length': Buffer.byteLength(data),
-      },
-    }, res => {
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => {
-        try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 300) }); }
-      });
-    });
-    req.on('error', reject);
-    req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
-    req.write(data);
-    req.end();
-  });
-}
-
-function fetchUrl(url, redirectCount) {
-  redirectCount = redirectCount || 0;
-  if (redirectCount > 5) return Promise.reject(new Error('too many redirects'));
-  return new Promise((resolve, reject) => {
-    const parsed = new URL(url);
-    const mod = parsed.protocol === 'https:' ? https : http;
-    const options = {
-      hostname: parsed.hostname,
-      path: parsed.pathname + parsed.search,
-      method: 'GET',
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
-        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-        'Accept-Language': 'en-US,en;q=0.5',
-      },
-    };
-    const req = mod.request(options, res => {
-      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
-        const loc = res.headers.location;
-        const redirectUrl = loc.startsWith('http') ? loc : `${parsed.protocol}//${parsed.hostname}${loc}`;
-        res.resume();
-        return fetchUrl(redirectUrl, redirectCount + 1).then(resolve).catch(reject);
-      }
-      let c = '';
-      res.on('data', d => c += d);
-      res.on('end', () => resolve({ status: res.statusCode, body: c }));
-    });
-    req.on('error', reject);
-    req.setTimeout(20000, () => { req.destroy(); reject(new Error('fetch timeout')); });
-    req.end();
-  });
-}
-
-function bucketPrice(pricePerM2) {
-  let best = null;
-  let bestDist = Infinity;
-  for (const b of BUCKETS) {
-    const dist = Math.abs(pricePerM2 - b.center);
-    if (dist < bestDist) { bestDist = dist; best = b.group; }
-  }
-  return best;
-}
-
-function extractPriceFromHtml(html) {
-  // Strategy 1: JSON-LD Product offers
-  // RW encodes base per-m2 USD price as e.g. "price":"76.4" in the Product JSON-LD block
-  const jsonldMatches = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi) || [];
-  for (const block of jsonldMatches) {
-    const inner = block.replace(/<script[^>]*>/, '').replace(/<\/script>/, '');
-    try {
-      const obj = JSON.parse(inner);
-      const items = Array.isArray(obj) ? obj : [obj];
-      for (const item of items) {
-        if (item['@type'] !== 'Product') continue;
-        const offers = item.offers;
-        if (!offers) continue;
-        const offerList = Array.isArray(offers) ? offers : [offers];
-        for (const offer of offerList) {
-          if (!offer.price) continue;
-          const p = parseFloat(offer.price);
-          if (p >= 40 && p <= 200) {
-            return { price: p, source: 'jsonld-product-offer' };
-          }
-        }
-      }
-    } catch (e) {}
-  }
-
-  // Strategy 2: inline numeric "price":67.45 (no quotes on value) in JS config
-  const numericMatch = html.match(/"price"\s*:\s*(\d{2,3}\.?\d*)\s*[,}]/);
-  if (numericMatch) {
-    const p = parseFloat(numericMatch[1]);
-    if (p >= 40 && p <= 200) {
-      return { price: p, source: 'script-numeric-price' };
-    }
-  }
-
-  // Strategy 3: "$X / sq ft" -> convert to m2
-  const sqftMatch = html.match(/\$\s*([\d.]+)\s*\/\s*sq\.?\s*ft/i);
-  if (sqftMatch) {
-    const p = parseFloat(sqftMatch[1]);
-    if (p > 1 && p < 30) {
-      return { price: p * 10.7639, source: 'sqft-pattern' };
-    }
-  }
-
-  return null;
-}
-
-async function getProductMeta(gid) {
-  const query = `{
-    product(id: "${gid}") {
-      id
-      title
-      rw_url: metafield(namespace: "custom", key: "rw_product_url") { value }
-      price_group: metafield(namespace: "custom", key: "price_group") { value }
-    }
-  }`;
-  const res = await gql({ query });
-  if (res.errors) throw new Error(JSON.stringify(res.errors));
-  return res.data && res.data.product;
-}
-
-async function writeMetafields(gid, group) {
-  const metafields = [
-    {
-      ownerId: gid,
-      namespace: 'custom',
-      key: 'price_group',
-      value: group,
-      type: 'single_line_text_field',
-    },
-    {
-      ownerId: gid,
-      namespace: 'custom',
-      key: 'cost_basis',
-      value: `${group} — portal-RRP-derived; P&S/Commercial extrapolated`,
-      type: 'single_line_text_field',
-    },
-  ];
-
-  if (group !== 'C3') {
-    metafields.push({
-      ownerId: gid,
-      namespace: 'custom',
-      key: 'cost_per_m2',
-      value: COST_PER_M2[group],
-      type: 'number_decimal',
-    });
-    metafields.push({
-      ownerId: gid,
-      namespace: 'custom',
-      key: 'material_options',
-      value: MATERIAL_OPTIONS[group],
-      type: 'json',
-    });
-  }
-
-  const mutation = `
-    mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
-      metafieldsSet(metafields: $metafields) {
-        metafields { key namespace value }
-        userErrors { field message }
-      }
-    }
-  `;
-  const res = await gql({ query: mutation, variables: { metafields } });
-  if (res.errors) throw new Error(JSON.stringify(res.errors));
-  const errs = res.data && res.data.metafieldsSet && res.data.metafieldsSet.userErrors;
-  if (errs && errs.length > 0) throw new Error('userErrors: ' + JSON.stringify(errs));
-  return res.data.metafieldsSet.metafields;
-}
-
-async function processProduct(gid) {
-  const meta = await getProductMeta(gid);
-  if (!meta) return { status: 'skip', reason: 'no product' };
-
-  const rwUrl = meta.rw_url && meta.rw_url.value;
-  if (!rwUrl) return { status: 'skip', reason: 'no rw_product_url' };
-
-  let pageData;
-  try {
-    pageData = await fetchUrl(rwUrl);
-  } catch (e) {
-    return { status: 'failed', reason: `fetch error: ${e.message}`, url: rwUrl };
-  }
-
-  if (pageData.status !== 200) {
-    return { status: 'failed', reason: `HTTP ${pageData.status}`, url: rwUrl };
-  }
-
-  const priceResult = extractPriceFromHtml(pageData.body);
-  if (!priceResult) {
-    console.error(`[UNKNOWN] ${gid.split('/').pop()} — no price found. URL: ${rwUrl}`);
-    // Defaulting to C3 (existing baseline)
-    await writeMetafields(gid, 'C3');
-    return { status: 'unknown', group: 'C3', url: rwUrl };
-  }
-
-  const group = bucketPrice(priceResult.price);
-  console.log(`[OK] ${gid.split('/').pop()} | ${meta.title} | $${priceResult.price.toFixed(2)}/m2 (${priceResult.source}) => ${group}`);
-
-  await writeMetafields(gid, group);
-  return { status: 'ok', group, price: priceResult.price, source: priceResult.source };
-}
-
-async function main() {
-  const results = { processed: 0, c2: 0, c3: 0, c4: 0, unknown: 0, failed: 0, notes: [] };
-
-  for (let i = 0; i < PRODUCT_IDS.length; i++) {
-    const gid = PRODUCT_IDS[i];
-    const shortId = gid.split('/').pop();
-    try {
-      const r = await processProduct(gid);
-      results.processed++;
-
-      if (r.status === 'skip') {
-        results.unknown++;
-        results.notes.push(`SKIP ${shortId}: ${r.reason}`);
-        console.log(`[SKIP] ${shortId}: ${r.reason}`);
-      } else if (r.status === 'unknown') {
-        results.unknown++;
-        results.notes.push(`UNKNOWN ${shortId}: no price on page, defaulted C3`);
-      } else if (r.status === 'failed') {
-        results.failed++;
-        results.notes.push(`FAIL ${shortId}: ${r.reason}`);
-        console.error(`[FAIL] ${shortId}: ${r.reason}`);
-      } else {
-        if (r.group === 'C2') results.c2++;
-        else if (r.group === 'C3') results.c3++;
-        else if (r.group === 'C4') results.c4++;
-      }
-    } catch (e) {
-      results.processed++;
-      results.failed++;
-      results.notes.push(`FAIL ${shortId}: ${e.message}`);
-      console.error(`[FAIL] ${shortId}: ${e.message}`);
-    }
-
-    // ~2 req/s
-    if (i < PRODUCT_IDS.length - 1) await sleep(500);
-  }
-
-  console.log('\n=== FINAL RESULTS ===');
-  console.log(JSON.stringify(results, null, 2));
-  return results;
-}
-
-main().catch(e => { console.error('FATAL:', e); process.exit(1); });

← 7debeb6 Stage GDrive cost-mine 2026-06-15: 772 rows (DW Exclusives 5  ·  back to Dw Yolo Loop  ·  chore: gitignore runtime artifacts + un-track gmc-425-result f5311b1 →