[object Object]

← back to Designerwallcoverings

wm-reprice: fix wrong-variant reprice + over-reverting undo (code review)

9d50ba16ae15e4a21824da7b0208b392b8c49728 · 2026-09-17 09:42:29 -0700 · Steve Abrams

reprice.mjs resolve(): drop the `|| edges[0]` fallback. Shopify sku: search
tokenizes on hyphens and this catalog has cross-product duplicate SKUs, so a
non-exact result set could reprice a DIFFERENT variant (the drift guard rechecks
the same wrong variant and passes). No exact match now returns null -> UNRESOLVED.

revert.mjs: read applied.csv (variants actually written) instead of the planned
restore-map.csv superset, with a restore-map fallback; and only revert a variant
still at the price we set (live == new_price), so it never clobbers a drift-skipped
or since-changed variant. Reports drift/not-found skips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WJBZbEV1wuUcnB8g5Qqg3

Files touched

Diff

commit 9d50ba16ae15e4a21824da7b0208b392b8c49728
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 17 09:42:29 2026 -0700

    wm-reprice: fix wrong-variant reprice + over-reverting undo (code review)
    
    reprice.mjs resolve(): drop the `|| edges[0]` fallback. Shopify sku: search
    tokenizes on hyphens and this catalog has cross-product duplicate SKUs, so a
    non-exact result set could reprice a DIFFERENT variant (the drift guard rechecks
    the same wrong variant and passes). No exact match now returns null -> UNRESOLVED.
    
    revert.mjs: read applied.csv (variants actually written) instead of the planned
    restore-map.csv superset, with a restore-map fallback; and only revert a variant
    still at the price we set (live == new_price), so it never clobbers a drift-skipped
    or since-changed variant. Reports drift/not-found skips.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011WJBZbEV1wuUcnB8g5Qqg3
---
 scripts/wm-reprice/reprice.mjs |  8 ++++++--
 scripts/wm-reprice/revert.mjs  | 33 +++++++++++++++++++++++++--------
 2 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/scripts/wm-reprice/reprice.mjs b/scripts/wm-reprice/reprice.mjs
index fc4fe00..e063a31 100644
--- a/scripts/wm-reprice/reprice.mjs
+++ b/scripts/wm-reprice/reprice.mjs
@@ -23,8 +23,12 @@ const MUT = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productV
 async function resolve(sku) {
   const d = await gql(`{ productVariants(first:3, query:${JSON.stringify('sku:' + sku)}){ edges{ node{ id sku price product{ id status } } } } }`);
   const edges = d?.productVariants?.edges || [];
-  // exact-sku match on the sellable variant
-  const m = edges.find(e => e.node.sku === sku) || edges[0];
+  // EXACT-sku match ONLY. Shopify's `sku:` search tokenizes on hyphens/spaces, so a query for
+  // "ABC-01" can return only near-matches ("ABC-011", "ABC-01-XL") with no exact hit — and this
+  // catalog carries cross-product duplicate SKUs. Falling back to edges[0] would reprice a
+  // DIFFERENT variant, and the drift guard below can't catch it (it re-checks the same wrong
+  // variant, so nowLive===r.live). No exact match -> UNRESOLVED (caller counts + skips it).
+  const m = edges.find(e => e.node.sku === sku);
   if (!m) return null;
   return { vgid: m.node.id, pid: m.node.product.id, status: m.node.product.status, live: parseFloat(m.node.price) };
 }
diff --git a/scripts/wm-reprice/revert.mjs b/scripts/wm-reprice/revert.mjs
index 02394f7..2aa61b2 100644
--- a/scripts/wm-reprice/revert.mjs
+++ b/scripts/wm-reprice/revert.mjs
@@ -1,29 +1,46 @@
 /* TK-11814 — REVERT the William Morris live reprice.
-   Reads scripts/wm-reprice/restore-map.csv (variant_gid,sku,old_price,new_price) and writes
-   every variant's OLD price back via productVariantsBulkUpdate. DRY by default; APPLY=1 writes.
-   This is the one-command undo referenced in the executed-reversible ledger.                */
+   Restores each variant's OLD price via productVariantsBulkUpdate. DRY by default; APPLY=1 writes.
+   This is the one-command undo referenced in the executed-reversible ledger.
+
+   SOURCE: prefer applied.csv = the variants reprice ACTUALLY wrote. restore-map.csv is the
+   PLANNED (pre-drift-filter) superset AND is overwritten on every reprice run, so reverting from
+   it (a) clobbers drift-skipped variants reprice never changed, and (b) loses the true original
+   after a 2nd reprice run. applied.csv is the correct per-run "what changed" record.
+   GUARD: only revert a variant still sitting at the price WE set (new_price). If it has moved
+   since (drift-skipped, or a later manual/other edit), skip it — reverting would clobber that
+   value. (scoped-ledger-rollback: the reversal set must be the set that was actually mutated.) */
 import fs from 'node:fs';
 import { gql } from '../lib/shopify.mjs';
 
 const APPLY = process.env.APPLY === '1';
 const DIR = new URL('.', import.meta.url).pathname;
 const SUF = process.env.SUF || '';
-const rows = fs.readFileSync(DIR + `restore-map${SUF}.csv`, 'utf8').trim().split('\n').slice(1).map(l => {
+const APPLIED = DIR + `applied${SUF}.csv`;
+const RESTORE = DIR + `restore-map${SUF}.csv`;
+const SRC = fs.existsSync(APPLIED) ? APPLIED : RESTORE;
+if (SRC === RESTORE) console.warn(`WARN: applied${SUF}.csv not found — falling back to restore-map${SUF}.csv (planned superset). The live current==new_price guard still prevents clobbering unchanged variants.`);
+const rows = fs.readFileSync(SRC, 'utf8').trim().split('\n').slice(1).map(l => {
   const [vgid, sku, oldp, newp] = l.split(',');
   return { vgid, sku, oldp: +oldp, newp: +newp };
 });
 const MUT = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid,variants:$variants){ productVariants{ id price } userErrors{ field message } } }`;
 
 (async () => {
-  console.log(`MODE: ${APPLY ? 'APPLY (reverting live)' : 'DRY (no writes — set APPLY=1)'} | variants: ${rows.length}\n`);
+  console.log(`MODE: ${APPLY ? 'APPLY (reverting live)' : 'DRY (no writes — set APPLY=1)'} | source: ${SRC.split('/').pop()} | variants: ${rows.length}\n`);
   const byPid = {};
+  let drift = 0, missing = 0;
   for (const r of rows) {
     const d = await gql(`{ productVariant(id:"${r.vgid}"){ price product{ id } } }`);
     const pid = d?.productVariant?.product?.id;
-    if (!pid) { console.error(`  skip ${r.sku}: not found`); continue; }
+    if (!pid) { missing++; console.error(`  skip ${r.sku}: not found`); continue; }
+    // GUARD: only revert if the variant is STILL at the price we set (new_price). Otherwise it
+    // was drift-skipped by reprice or changed since — reverting to old_price would clobber it.
+    const nowLive = parseFloat(d.productVariant.price);
+    if (nowLive !== r.newp) { drift++; console.error(`  skip ${r.sku}: live $${nowLive} != our new $${r.newp} (changed since — not reverting)`); continue; }
     (byPid[pid] ||= []).push(r);
   }
-  if (!APPLY) { console.log(`DRY: would restore ${rows.length} variants to their old prices. Re-run APPLY=1.`); return; }
+  const willRevert = Object.values(byPid).reduce((s, v) => s + v.length, 0);
+  if (!APPLY) { console.log(`\nDRY: would restore ${willRevert} of ${rows.length} variants to their old prices (skip ${drift} drifted, ${missing} not-found). Re-run APPLY=1.`); return; }
   let wrote = 0, err = 0;
   for (const [pid, vs] of Object.entries(byPid)) {
     const r = await gql(MUT, { pid, variants: vs.map(v => ({ id: v.vgid, price: String(v.oldp) })) });
@@ -31,5 +48,5 @@ const MUT = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productV
     if (ue.length) { err += vs.length; console.error(`  ERR ${pid}: ${JSON.stringify(ue)}`); continue; }
     wrote += (r.productVariantsBulkUpdate.productVariants || []).length;
   }
-  console.log(`\n=== REVERTED === wrote:${wrote} err:${err}`);
+  console.log(`\n=== REVERTED === wrote:${wrote} err:${err} skipped:${drift + missing} (drift:${drift} not-found:${missing})`);
 })();

← 6c2f698 auto-data-snapshot: 2026-09-17T08:16:42 (5 data files) — scr  ·  back to Designerwallcoverings  ·  auto-data-snapshot: 2026-09-17T09:53:49 (1 data files) — dat 479d315 →