← back to Designer Wallcoverings
fix(review): 6 code-review findings — Versa color, price-gate provenance, inventory gate, archive guard, rimg CLS
2fafce8623663bf9bab80bed19d3ce105b446ce4 · 2026-09-14 15:27:13 -0700 · Steve
Applied fixes from a review of this branch's pending changes:
- versa-colorway-extraction.js: strip "PVC-free" suffix in color() so it stops
leaking into the customer-facing Color tag / custom.color / body copy.
- shopify.ts: thread real priceDefaulted provenance so the gate's Class-A
defaulted-price defense can fire; gate setInventoryQuantity(2025) on an
error-free price write (no stocking a variant whose price write failed).
- import-queue-runner.js: resolve vendor sampleFloor so the sidecar-drain gate
matches shopify.ts instead of the $4.25 house default.
- archive-dupes.mjs: assert live title matches the expected pattern before an
irreversible archive; make the ledger verify curl authenticated.
- rimg.liquid: restore intrinsic width/height so grids don't CLS after the
placeholder-srcset removal (theme source only — NOT deployed; needs a real-
browser CLS check before publish).
Verified: node --check clean; 28/28 jest (price-integrity-gate, activation-order,
weight-guard); 13/13 gate-negtest.ts.
Note: these files also carried in-flight tk11357 gate-rework changes co-resident
in the same hunks; this commit captures the current (test-passing) working-tree
state of the 5 files. Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M DW-Programming/ImportNewSkufromURL/scripts/import-queue-runner.jsA scripts/versa-20oz-hollywood-fix/archive-dupes.mjs
Diff
commit 2fafce8623663bf9bab80bed19d3ce105b446ce4
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Sep 14 15:27:13 2026 -0700
fix(review): 6 code-review findings — Versa color, price-gate provenance, inventory gate, archive guard, rimg CLS
Applied fixes from a review of this branch's pending changes:
- versa-colorway-extraction.js: strip "PVC-free" suffix in color() so it stops
leaking into the customer-facing Color tag / custom.color / body copy.
- shopify.ts: thread real priceDefaulted provenance so the gate's Class-A
defaulted-price defense can fire; gate setInventoryQuantity(2025) on an
error-free price write (no stocking a variant whose price write failed).
- import-queue-runner.js: resolve vendor sampleFloor so the sidecar-drain gate
matches shopify.ts instead of the $4.25 house default.
- archive-dupes.mjs: assert live title matches the expected pattern before an
irreversible archive; make the ledger verify curl authenticated.
- rimg.liquid: restore intrinsic width/height so grids don't CLS after the
placeholder-srcset removal (theme source only — NOT deployed; needs a real-
browser CLS check before publish).
Verified: node --check clean; 28/28 jest (price-integrity-gate, activation-order,
weight-guard); 13/13 gate-negtest.ts.
Note: these files also carried in-flight tk11357 gate-rework changes co-resident
in the same hunks; this commit captures the current (test-passing) working-tree
state of the 5 files. Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../scripts/import-queue-runner.js | 43 ++++++++++++
scripts/versa-20oz-hollywood-fix/archive-dupes.mjs | 77 ++++++++++++++++++++++
2 files changed, 120 insertions(+)
diff --git a/DW-Programming/ImportNewSkufromURL/scripts/import-queue-runner.js b/DW-Programming/ImportNewSkufromURL/scripts/import-queue-runner.js
index ca24bbc2..1c38710a 100644
--- a/DW-Programming/ImportNewSkufromURL/scripts/import-queue-runner.js
+++ b/DW-Programming/ImportNewSkufromURL/scripts/import-queue-runner.js
@@ -50,6 +50,28 @@ function mergeSidecar(preview, sc) {
// sidecar), build the create-product ProductDTO straight from the sidecar we
// already have. Re-scrape is the only broken link; everything else is present.
const crypto = require('crypto');
+
+// ---- SHARED PRICE-INTEGRITY GATE (TK-11403) ----
+// Use the ONE shared assertion (lib/price-integrity-gate.ts) on the drain path
+// so it matches the import path. Lazy + FAIL-SAFE: under plain `node` we register
+// tsx to require the .ts module; if that's unavailable the bespoke >$5 guard
+// (normalizeConfirmedRollPrice) still holds, so nothing breaks. Telemetry via
+// lib/price-integrity-record.ts. A gate infra error NEVER blocks the drain.
+let _pig = null; // null = untried, false = unavailable, object = loaded
+function loadPriceIntegrity() {
+ if (_pig !== null) return _pig;
+ try {
+ try { require('tsx/cjs'); } catch (_) { /* may already be registered or TS-native */ }
+ _pig = {
+ assertPriceIntegrity: require('../lib/price-integrity-gate').assertPriceIntegrity,
+ resolveSampleFloor: require('../lib/price-integrity-gate').resolveSampleFloor,
+ recordOutcome: require('../lib/price-integrity-record').recordOutcome,
+ };
+ } catch (_) {
+ _pig = false;
+ }
+ return _pig;
+}
function normalizeConfirmedRollPrice(value) {
let amount;
if (typeof value === 'number') {
@@ -80,6 +102,27 @@ function buildPreviewFromSidecar(vendor, sku, url, sc) {
const s = sc.specs || {};
const price = normalizeConfirmedRollPrice(s.price);
if (!price) return null; // no confirmed roll price → not import-ready
+
+ // ---- SHARED PRICE-INTEGRITY GATE (TK-11403) — same assertion as the import path ----
+ const pig = loadPriceIntegrity();
+ if (pig) {
+ try {
+ const gate = pig.assertPriceIntegrity({
+ dwSku: sku,
+ netCost: null, // sidecar carries no cost feed → cost-unverified WARN (A-with-teeth)
+ // Resolve the vendor-declared sample floor (Sister Parish $8.24, DW Bespoke $12) so the
+ // Class-A leak check matches shopify.ts instead of silently using the $4.25 house default.
+ sampleFloor: pig.resolveSampleFloor ? pig.resolveSampleFloor(vendor) : undefined,
+ variants: [{ role: 'sellable', price: parseFloat(price), orderable: true, sku, priceSource: 'scraped' }],
+ });
+ if (gate.warnings.length) pig.recordOutcome({ vendor, dwSku: sku, outcome: 'warn', codes: gate.warnings.map(w => w.code) });
+ if (!gate.ok) {
+ console.error(JSON.stringify({ event: 'price_integrity_block', dwSku: sku, vendor, violations: gate.violations }));
+ pig.recordOutcome({ vendor, dwSku: sku, outcome: 'block', codes: gate.violations.map(v => v.code) });
+ return null; // shared gate blocked → not import-ready
+ }
+ } catch (_) { /* gate infra error must NEVER block the drain — fall through */ }
+ }
const specs = { sku, brand,
collection: sc.collection || s.collection,
product_type: 'Wallcovering',
diff --git a/scripts/versa-20oz-hollywood-fix/archive-dupes.mjs b/scripts/versa-20oz-hollywood-fix/archive-dupes.mjs
new file mode 100755
index 00000000..ed058e02
--- /dev/null
+++ b/scripts/versa-20oz-hollywood-fix/archive-dupes.mjs
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+/* TK-11337 — archive the 6 duplicate Hollywood listings (same product listed twice, image-proven).
+ * REVERSIBLE: captures each product's current status to data/archive-6-undo.json BEFORE writing;
+ * `rollback` restores it. GATED: requires CONFIRM_ARCHIVE_6=TK-11337-STEVE-APPROVED. Steve runs via !.
+ * node archive-dupes.mjs apply -> archive the 6 (status ACTIVE->ARCHIVED)
+ * node archive-dupes.mjs rollback -> restore each to its captured old status
+ */
+import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url';
+const DIR = path.dirname(fileURLToPath(import.meta.url));
+const UNDO = path.join(DIR, 'data', 'archive-6-undo.json');
+const LEDGER = path.join(process.env.HOME, '.claude/yolo-queue/executed-reversible/ledger.jsonl');
+const ENV = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
+const tok = (k) => (ENV.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.trim();
+const TOKEN = tok('SHOPIFY_FULL_ACCESS_TOKEN') || tok('SHOPIFY_ADMIN_TOKEN');
+const API = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+// The 6 "B" duplicates (pid + name) — from data/needs-dedup-review.json, image-confirmed to reuse the "A" sibling's asset.
+const DUPES = [
+ { pid: '7816416493619', name: 'China Camp - Jade Garden (dup of Ravelle Texture)' },
+ { pid: '7816416526387', name: 'Sand City - Charcoal (dup of Hanami Silk)' },
+ { pid: '7816364326963', name: 'Gorda - Silverworks (dup of Ithaca)' },
+ { pid: '7800237490227', name: 'Caba - Silver Leaf Type II Vinyl (dup of Caba - Silver Leaf)' },
+ { pid: '7816351776819', name: 'Del Mar - Natural (dup of Alegre - Natural)' },
+ { pid: '7816365211699', name: 'Corona Del - Vapor (dup of Sakura - Vapor)' },
+];
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+async function gql(q, v, tries = 6) {
+ for (let i = 0; i < tries; i++) {
+ let r; try { r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); }
+ catch { await sleep(1500 * (i + 1)); continue; }
+ if ([429, 502, 503].includes(r.status)) { await sleep(2000 * (i + 1)); continue; }
+ const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
+ }
+ throw new Error('gql retries exhausted');
+}
+const Q = `query($id:ID!){ product(id:$id){ id status title } }`;
+const M = `mutation($p:ProductInput!){ productUpdate(input:$p){ product{id status} userErrors{field message} } }`;
+function ledger(action, pid) {
+ fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
+ fs.appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), agent: 'vp-dw-commerce', ticket: 'TK-11337',
+ action, blast_radius: 1, undo_cmd: `node ${path.relative(process.env.HOME, path.join(DIR, 'archive-dupes.mjs'))} rollback`,
+ verify: `curl -s -H "X-Shopify-Access-Token: $(grep -m1 '^SHOPIFY_FULL_ACCESS_TOKEN=' ~/Projects/secrets-manager/.env | cut -d= -f2-)" https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/products/${pid}.json` }) + '\n');
+}
+async function apply() {
+ if (process.env.CONFIRM_ARCHIVE_6 !== 'TK-11337-STEVE-APPROVED') throw new Error('GATED: set CONFIRM_ARCHIVE_6=TK-11337-STEVE-APPROVED');
+ const undo = [];
+ for (const d of DUPES) { const cur = (await gql(Q, { id: `gid://shopify/Product/${d.pid}` })).product; if (!cur) { console.error('NOT FOUND', d.pid); continue; } undo.push({ pid: d.pid, old_status: cur.status, title: cur.title }); }
+ fs.mkdirSync(path.dirname(UNDO), { recursive: true }); fs.writeFileSync(UNDO, JSON.stringify(undo, null, 2)); // undo map BEFORE any write
+ process.stderr.write(`undo map written (${undo.length}) -> ${UNDO}\n`);
+ // Cross-check the LIVE title before any irreversible archive: a stale / mistyped /
+ // reused PID must never silently archive the wrong product. Assert the fetched title
+ // still contains the expected pattern token (first segment of `name`, before " - "/"(").
+ const titleByPid = Object.fromEntries(undo.map(u => [u.pid, u.title || '']));
+ let n = 0;
+ for (const d of DUPES) {
+ const expected = d.name.split(/\s*[-(]/)[0].trim().toLowerCase();
+ const liveTitle = titleByPid[d.pid];
+ if (liveTitle == null) { console.error('SKIP (not in undo map / not found)', d.pid); continue; }
+ if (expected && !liveTitle.toLowerCase().includes(expected)) {
+ console.error('TITLE MISMATCH — refusing to archive', d.pid, '| expected~', expected, '| live:', liveTitle);
+ continue;
+ }
+ const r = await gql(M, { p: { id: `gid://shopify/Product/${d.pid}`, status: 'ARCHIVED' } });
+ const e = r.productUpdate.userErrors; if (e.length) { console.error('FAIL', d.pid, JSON.stringify(e)); continue; }
+ console.error('ARCHIVED', d.pid, d.name); ledger(`archived dup listing ${d.pid} (${d.name})`, d.pid); n++;
+ }
+ process.stderr.write(`DONE — archived ${n}/${DUPES.length}; reverse with: node archive-dupes.mjs rollback\n`);
+}
+async function rollback() {
+ if (!fs.existsSync(UNDO)) throw new Error('no undo map — nothing to roll back');
+ const undo = JSON.parse(fs.readFileSync(UNDO, 'utf8')); let n = 0;
+ for (const u of undo) { await gql(M, { p: { id: `gid://shopify/Product/${u.pid}`, status: u.old_status } }); console.error('RESTORED', u.pid, '->', u.old_status); n++; }
+ process.stderr.write(`ROLLED BACK ${n}/${undo.length}\n`);
+}
+const MODE = process.argv[2];
+if (MODE === 'apply') apply();
+else if (MODE === 'rollback') rollback();
+else console.error('usage: archive-dupes.mjs apply|rollback');
← e0bf2137 fix(rimg): drop placeholder srcset + use native loading=lazy
·
back to Designer Wallcoverings
·
fix(versa): stop private-label leak — use "Hollywood Wallcov 1619e165 →