[object Object]

← back to Tk10630 Sku Suffix Canary

CORRECTION: Option C retracted (always-pl-momentum); 70 reverted to DWHD; scheme = Hollywood private-label prefix (Option A)

ae72fe945d2b907eed175b30a1a65b3c19c212d2 · 2026-08-17 16:28:59 -0700 · steve

Files touched

Diff

commit ae72fe945d2b907eed175b30a1a65b3c19c212d2
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Aug 17 16:28:59 2026 -0700

    CORRECTION: Option C retracted (always-pl-momentum); 70 reverted to DWHD; scheme = Hollywood private-label prefix (Option A)
---
 DESIGN-generator-real-code.md | 15 +++++++++++++++
 apply-backfill.mjs            | 42 ++++++++++++++++++++++++++++++++++++++++++
 revert-backfill.mjs           | 34 ++++++++++++++++++++++++++++++++++
 3 files changed, 91 insertions(+)

diff --git a/DESIGN-generator-real-code.md b/DESIGN-generator-real-code.md
index db3c109..e8d03aa 100644
--- a/DESIGN-generator-real-code.md
+++ b/DESIGN-generator-real-code.md
@@ -90,3 +90,18 @@ Verified: 20,000 feed records → 20,000 distinct `number`s, **0 cross-category
 (wallcovering + acoustic + textile share one non-overlapping number space). Option C (bare
 `number` SKU) is collision-safe within the current catalog. Caveat: the feed API caps at 20k
 records; if Momentum's true catalog exceeds 20k, re-verify before go-live. **RECOMMENDATION: Option C.**
+
+## ⚠ CORRECTION (Steve: "always pl momentum", 2026-08-17) — Option C RETRACTED
+The customer-facing SKU must ALWAYS carry a Hollywood PRIVATE-LABEL prefix; the raw Momentum
+`number` must NOT be the visible SKU (it's a competitor-traceable supplier code).
+- **Option C (bare number) is WRONG** — it exposes Momentum's own product number customer-facing.
+  (Tested live on 782 → caught after 70 → all 70 REVERTED to DWHD, 0 errors, no harm.)
+- **CORRECTED SCHEME = Option A:** customer-facing `dw_sku`/variant = a Hollywood private-label
+  prefix + number (e.g. `HW-<number>` or the legacy per-collection prefix like `HWC-`), which
+  is private-labeling — NOT the brand-as-vendor sin. The real Momentum `number` lives ONLY in
+  the internal `manufacturer_sku` metafield (never customer-facing).
+- Cody's "HW- is a brand prefix" objection is OVERRULED by this rule: the brand prefix is the
+  REQUIRED private-label wrapper. The original sin was the *sequential DW-vendor mint* (DWHD)
+  divorced from the real product — not the presence of a brand prefix.
+- OPEN for Steve: prefix = single `HW-` for all, or preserve/derive the legacy per-collection
+  prefixes (HWC/XWH/NOC…)? And is the numeric part the Momentum `number` or a DW-assigned one?
diff --git a/apply-backfill.mjs b/apply-backfill.mjs
new file mode 100644
index 0000000..0a3ba29
--- /dev/null
+++ b/apply-backfill.mjs
@@ -0,0 +1,42 @@
+// Apply the Option C backfill from generator-backfill-plan.json. DRY default; --apply writes.
+// Per product (serial): dw_sku metafields (global/dwc/custom)=number + variant SKUs=number-suf.
+// Resolves fresh IDs per product. Resumable via done-backfill.jsonl. --only / --limit for staging.
+import { readFileSync, appendFileSync, existsSync } from 'node:fs';
+import { gql } from './shopify.mjs';
+const APPLY = process.argv.includes('--apply');
+const LIMIT = Number((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || Infinity);
+const ONLY = (process.argv.find(a => a.startsWith('--only=')) || '').split('=')[1] || null;
+const DONE = 'done-backfill.jsonl';
+const plan = JSON.parse(readFileSync('generator-backfill-plan.json', 'utf8')).filter(p => !p.COLLISION);
+const done = new Set();
+if (existsSync(DONE)) for (const l of readFileSync(DONE, 'utf8').split('\n')) if (l.trim()) done.add(JSON.parse(l).handle);
+let work = plan.filter(p => !done.has(p.handle));
+if (ONLY) work = work.filter(p => p.handle.includes(ONLY));
+if (work.length > LIMIT) work = work.slice(0, LIMIT);
+console.log(`[backfill] mode=${APPLY ? 'LIVE' : 'DRY'} plan=${plan.length} todo=${work.length}`);
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function q(query, v) { for (let a = 0; ; a++) { try { return (await gql(query, v)).data; } catch (e) { if (/THROTTLED|<html/i.test(e.message) && a < 8) { await sleep(1500 * (a + 1)); continue; } throw e; } } }
+const INV = `mutation($id:ID!,$sku:String!){ inventoryItemUpdate(id:$id, input:{sku:$sku}){ userErrors{message} } }`;
+const MFS = `mutation($m:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$m){ userErrors{message} } }`;
+
+let ok = 0, err = 0;
+for (const p of work) {
+  try {
+    const d = await q(`query{ productByHandle(handle:"${p.handle}"){ id variants(first:6){ nodes{ sku inventoryItem{ id } } } } }`);
+    const prod = d.productByHandle; if (!prod) { err++; console.log('✗ gone', p.handle); continue; }
+    const errs = [];
+    // variant SKUs: match current sku -> target from plan.varChanges
+    for (const c of p.varChanges) {
+      const v = prod.variants.nodes.find(x => x.sku === c.from);
+      if (!v) continue; // already changed or not found
+      if (APPLY) { const r = await q(INV, { id: v.inventoryItem.id, sku: c.to }); if (r.inventoryItemUpdate.userErrors.length) errs.push('var:' + JSON.stringify(r.inventoryItemUpdate.userErrors)); }
+    }
+    // dw_sku metafields -> the number
+    const m = ['global', 'dwc', 'custom'].map(ns => ({ ownerId: prod.id, namespace: ns, key: 'dw_sku', type: 'single_line_text_field', value: String(p.number) }));
+    if (APPLY) { const r = await q(MFS, { m }); if (r.metafieldsSet.userErrors.length) errs.push('mf:' + JSON.stringify(r.metafieldsSet.userErrors)); }
+    if (errs.length) { err++; console.log('✗', p.handle, errs.join(';')); }
+    else { ok++; if (APPLY) appendFileSync(DONE, JSON.stringify({ handle: p.handle, number: p.number }) + '\n'); }
+  } catch (e) { err++; console.log('✗', p.handle, e.message.slice(0, 90)); }
+  if ((ok + err) % 50 === 0) process.stderr.write(`  ${ok + err}/${work.length}\n`);
+}
+console.log(`[backfill] DONE ok=${ok} err=${err} ${APPLY ? '(written)' : '(dry)'}`);
diff --git a/revert-backfill.mjs b/revert-backfill.mjs
new file mode 100644
index 0000000..f051039
--- /dev/null
+++ b/revert-backfill.mjs
@@ -0,0 +1,34 @@
+// Revert the 70 mis-backfilled products (bare Momentum number -> leak) back to their
+// prior DWHD codes. Restores variant SKUs to plan.varChanges.from + dw_sku metafields to
+// the DWHD base. Corrective write for the Option-C-was-wrong ('always pl momentum') error.
+import { readFileSync } from 'node:fs';
+import { gql } from './shopify.mjs';
+const plan = JSON.parse(readFileSync('generator-backfill-plan.json', 'utf8'));
+const doneHandles = new Set(readFileSync('done-backfill.jsonl', 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l).handle));
+const work = plan.filter(p => doneHandles.has(p.handle) && !p.COLLISION);
+const APPLY = process.argv.includes('--apply');
+console.log(`[revert] mode=${APPLY ? 'LIVE' : 'DRY'} to-revert=${work.length}`);
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function q(query, v) { for (let a = 0; ; a++) { try { return (await gql(query, v)).data; } catch (e) { if (/THROTTLED|<html/i.test(e.message) && a < 8) { await sleep(1500 * (a + 1)); continue; } throw e; } } }
+const INV = `mutation($id:ID!,$sku:String!){ inventoryItemUpdate(id:$id, input:{sku:$sku}){ userErrors{message} } }`;
+const MFS = `mutation($m:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$m){ userErrors{message} } }`;
+let ok = 0, err = 0;
+for (const p of work) {
+  // DWHD base from the original variant sku (.from), e.g. DWHD-503061-Sample -> DWHD-503061
+  const base = (p.varChanges[0]?.from || '').replace(/-(sample|yard|roll)$/i, '');
+  if (!/^DWH/i.test(base)) { console.log('skip (no DWHD from):', p.handle); continue; }
+  try {
+    const d = await q(`query{ productByHandle(handle:"${p.handle}"){ id variants(first:6){ nodes{ sku inventoryItem{ id } } } } }`);
+    const prod = d.productByHandle; if (!prod) { err++; continue; }
+    const errs = [];
+    for (const c of p.varChanges) {
+      const v = prod.variants.nodes.find(x => x.sku === c.to); // current = the bare-number target
+      if (!v) continue;
+      if (APPLY) { const r = await q(INV, { id: v.inventoryItem.id, sku: c.from }); if (r.inventoryItemUpdate.userErrors.length) errs.push(JSON.stringify(r.inventoryItemUpdate.userErrors)); }
+    }
+    const m = ['global', 'dwc', 'custom'].map(ns => ({ ownerId: prod.id, namespace: ns, key: 'dw_sku', type: 'single_line_text_field', value: base }));
+    if (APPLY) { const r = await q(MFS, { m }); if (r.metafieldsSet.userErrors.length) errs.push(JSON.stringify(r.metafieldsSet.userErrors)); }
+    if (errs.length) { err++; console.log('✗', p.handle, errs.join(';')); } else ok++;
+  } catch (e) { err++; console.log('✗', p.handle, e.message.slice(0, 80)); }
+}
+console.log(`[revert] DONE ok=${ok} err=${err} ${APPLY ? '(written back to DWHD)' : '(dry)'}`);

← 661478b generator Option C backfill dry-run (782 products, 0 collisi  ·  back to Tk10630 Sku Suffix Canary  ·  DTD verdict B (5/5): leave existing DWHD, fix generator forw 5c8d27f →