← 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
M scripts/wm-reprice/reprice.mjsM scripts/wm-reprice/revert.mjs
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 →