← back to Dw Retail Halving Preview
READ-ONLY retail-halving preview tool + inert gated live-write scaffold
79b5ac997f09476c66169d9e8578b70a8f8eedc8 · 2026-06-26 12:43:57 -0700 · Steve
Preview reads local dw_unified mirror, excludes Kravet-umbrella (MAP), zero-price
guards halved<=0. Live-write scaffold is inert (refuses --execute), snapshots
rollback CSV first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A .gitignoreA README.mdA apply-halving-LIVE.jsA halve-preview.js
Diff
commit 79b5ac997f09476c66169d9e8578b70a8f8eedc8
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 12:43:57 2026 -0700
READ-ONLY retail-halving preview tool + inert gated live-write scaffold
Preview reads local dw_unified mirror, excludes Kravet-umbrella (MAP), zero-price
guards halved<=0. Live-write scaffold is inert (refuses --execute), snapshots
rollback CSV first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
.gitignore | 8 +++
README.md | 36 +++++++++++
apply-halving-LIVE.js | 88 +++++++++++++++++++++++++++
halve-preview.js | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 297 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4608ff7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# dw-retail-halving-preview
+
+READ-ONLY dry-run tooling for Steve's ambiguous request: *"go and cut the retail
+prices in half from online for price…"* on the DW Shopify catalog.
+
+DTD panel committed **verdict B** (2–1, Codex+Claude over Qwen): do the safe,
+reversible prep (preview + rollback design + gated memo) now; do NOT guess the
+scope or the meaning of "from online" into a live change.
+
+## Files
+- `halve-preview.js` — READ-ONLY. Reads the LOCAL `dw_unified` mirror
+ (`shopify_products`) via `psql`, never writes anything, never touches Shopify
+ or the Kamatera-canonical DB. Emits a CSV: `dw_sku, vendor, current_retail,
+ halved_retail, sample_price, excluded_reason`.
+ - Excludes every Kravet-umbrella brand (MAP-priced) → `excluded_reason=MAP`.
+ - Skips rows where halved price would be 0/null → `excluded_reason=zero_price_guard`.
+ - `halved_retail = round(current_retail / 2, 2)` for eligible rows.
+- `apply-halving-LIVE.js` — GATED live-write executor. **INERT** in this build;
+ refuses `--execute`. Snapshots every targeted variant's live price to a
+ rollback CSV BEFORE any update. Implement + run only on Steve's explicit go.
+
+## Usage
+```
+node halve-preview.js [--status ACTIVE] [--vendor "Thibaut"]... [--limit 50] \
+ [--price-col price|retail_price|min_variant_price] \
+ [--order vendor|random] [--out /abs/path.csv]
+```
+
+## Price-source note ("from online")
+In the local mirror, the customer-facing online variant price is `price`
+(18,541 ACTIVE populated). `retail_price` is sparse (7,650). The tool defaults
+to `price`. The live write must re-read the authoritative current price from
+Shopify at write-time (ground truth), not this mirror.
+
+## Cost
+$0 (local psql + node only; no paid API).
diff --git a/apply-halving-LIVE.js b/apply-halving-LIVE.js
new file mode 100644
index 0000000..dddf13c
--- /dev/null
+++ b/apply-halving-LIVE.js
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/*
+ * apply-halving-LIVE.js — GATED live-write executor. *** INERT BY DEFAULT ***
+ *
+ * This script is a SCAFFOLD. It will NOT write to Shopify or to any database
+ * unless invoked with the explicit flags below AND it has a vetted preview CSV.
+ * It is committed for review only; do NOT run it until Steve has answered the
+ * three open questions in the pending-approval memo and explicitly says "go".
+ *
+ * Safety design (in order):
+ * 1. INPUT = a vetted preview CSV produced by halve-preview.js (eligible rows only).
+ * 2. SNAPSHOT-FIRST: before ANY update, write a rollback CSV capturing every
+ * targeted variant's CURRENT live online price (read live from Shopify, the
+ * ground truth at write-time — NOT the possibly-stale local mirror).
+ * Columns: dw_sku, variant_id, inventory_item_id, current_price, intended_halved_price.
+ * The rollback CSV is the instant-restore artifact.
+ * 3. RE-VERIFY each row at write-time: vendor still non-MAP, halved>0, and the
+ * LIVE current price still matches what the preview halved (drift guard).
+ * 4. BATCH writes via productVariantsBulkUpdate (Shopify 2024-10), batch size
+ * <= 100 variants, >= 90s gap between batches (DW bulk-push rule), respect
+ * the 1k variant/day cap until the store passes 50K+ variants.
+ * 5. VARIANT BUDGET: price updates do NOT create variants, so no upload-budget
+ * take is required; if a future variant is created, take()+refund() per the
+ * DW budget.cjs refund rule.
+ *
+ * ROLLBACK (separate verb): --rollback <rollback.csv> restores each variant's
+ * current_price from the snapshot, same batch/gap rules.
+ *
+ * Usage (all gated — refuses without --i-have-steve-go):
+ * node apply-halving-LIVE.js --preview <vetted.csv> --snapshot-out <rollback.csv> --dry
+ * node apply-halving-LIVE.js --preview <vetted.csv> --snapshot-out <rollback.csv> --execute --i-have-steve-go
+ * node apply-halving-LIVE.js --rollback <rollback.csv> --execute --i-have-steve-go
+ *
+ * Cost: Shopify Admin API calls are free (no per-call charge); $0 paid-API.
+ */
+
+'use strict';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-10';
+const BATCH_SIZE = 100; // variants per productVariantsBulkUpdate
+const BATCH_GAP_MS = 90_000; // >= 90s between batches (DW rule)
+const DAILY_VARIANT_CAP = 1000; // until store passes 50K+ variants
+
+function die(msg) { console.error('REFUSING: ' + msg); process.exit(3); }
+
+const args = process.argv.slice(2);
+const has = (f) => args.includes(f);
+const val = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
+
+// -------------------------------------------------------------------------
+// HARD GATE: this scaffold will not perform a live write in this build.
+// Removing this block is a deliberate, Steve-approved action — not a default.
+// -------------------------------------------------------------------------
+if (has('--execute')) {
+ if (!has('--i-have-steve-go')) {
+ die('--execute requires --i-have-steve-go (explicit Steve approval token). None supplied.');
+ }
+ die('Live-write path is intentionally not implemented in this committed scaffold. '
+ + 'Implement Shopify productVariantsBulkUpdate + snapshot here ONLY after the '
+ + 'pending-approval memo is signed and Steve says go.');
+}
+
+console.log(`apply-halving-LIVE.js scaffold (INERT). Shop=${SHOP} api=${API_VERSION}`);
+console.log(`Batch=${BATCH_SIZE} gap=${BATCH_GAP_MS}ms cap=${DAILY_VARIANT_CAP}/day`);
+console.log('Run with --dry once implemented to preview the snapshot+plan; live writes stay gated.');
+console.log('cost: $0 (no API calls in scaffold)');
+
+/*
+ * --- IMPLEMENTATION NOTES for the gated build (left as comments on purpose) ---
+ *
+ * snapshot(previewRows):
+ * for each dw_sku -> resolve variant_id + inventoryItem.id via products query
+ * read LIVE current variant price (ground truth)
+ * append {dw_sku, variant_id, inventory_item_id, current_price, intended_halved_price}
+ * to snapshot-out BEFORE any mutation; flush to disk per batch.
+ *
+ * applyBatch(batch):
+ * mutation productVariantsBulkUpdate(productId, variants:[{id, price}]) — 2024-10.
+ * verify userErrors empty; on any error, STOP the whole run (fail-closed).
+ *
+ * rollback(snapshotCsv):
+ * for each row, productVariantsBulkUpdate price = current_price (the saved value).
+ *
+ * token: SHOPIFY_ADMIN_TOKEN via secrets (read_products + write_products). Never
+ * hardcode. PostgreSQL-before-Shopify: also patch dw_unified.shopify_products.price
+ * in the same transaction window so the mirror does not drift.
+ */
diff --git a/halve-preview.js b/halve-preview.js
new file mode 100644
index 0000000..12f4301
--- /dev/null
+++ b/halve-preview.js
@@ -0,0 +1,165 @@
+#!/usr/bin/env node
+/*
+ * halve-preview.js — READ-ONLY dry-run preview for the "cut retail prices in half" request.
+ *
+ * Reads the LOCAL dw_unified mirror (shopify_products) via psql, NEVER writes anything,
+ * NEVER touches Shopify, NEVER touches the Kamatera-canonical DB. Emits a CSV with:
+ * dw_sku, vendor, current_retail, halved_retail, sample_price, excluded_reason
+ *
+ * Rules baked in:
+ * - Kravet-umbrella brands are MAP-priced -> halving breaches MAP -> excluded_reason=MAP
+ * - Any row whose halved price would be 0/null -> excluded_reason=zero_price_guard
+ * - Eligible rows: halved_retail = round(current_retail / 2, 2)
+ *
+ * Usage:
+ * node halve-preview.js [--status ACTIVE] [--vendor "Thibaut"]... [--limit 50]
+ * [--price-col price|retail_price|min_variant_price]
+ * [--out /abs/path.csv]
+ *
+ * Cost: $0 (local psql + node, no paid API).
+ */
+
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+
+// ---- Kravet-umbrella MAP exclusion (case-insensitive, word-anchored) -------
+// Matches the actual vendor strings seen in dw_unified. Deliberately precise so
+// "Home Couture" (NOT Kravet) does not get swept in by a bare /couture/.
+const MAP_PATTERNS = [
+ /\bkravet\b/i, // Kravet, Kravet Couture, Kravet Design, Kravet Contract, Kravet Basics
+ /\blee\s+jofa\b/i, // Lee Jofa, Lee Jofa Modern
+ /\bgroundworks?\b/i, // Groundworks
+ /\bbrunschwig\b/i, // Brunschwig & Fils
+ /\bcole\s*(?:&|and)\s*son\b/i, // Cole & Son
+ /\bgp\s*(?:&|and)\s*j\s*baker\b/i,// GP & J Baker
+ /\bcolefax\b/i, // Colefax and Fowler
+ /\bclarke\s*(?:&|and)\s*clarke\b/i,// Clarke & Clarke / Clarke And Clarke
+ /\bmulberry\b/i, // Mulberry (Home)
+ /\bthreads\b/i, // Threads
+ /\bbaker\s+lifestyle\b/i, // Baker Lifestyle
+ /\bandrew\s+martin\b/i, // Andrew Martin
+ /\bnicolette\s+mayer\b/i, // Nicolette Mayer (also hard-archived)
+ /\baerin\b/i, // Aerin
+ /\bbarclay\s+butera\b/i, // Barclay Butera
+ /\bthom\s+filicia\b/i, // Thom Filicia
+];
+
+function isMapVendor(vendor) {
+ if (!vendor) return false;
+ return MAP_PATTERNS.some((re) => re.test(vendor));
+}
+
+const SAMPLE_PRICE = '4.25'; // DW standard sample price (the {DW_SKU}-Sample variant)
+
+// ---- arg parsing -----------------------------------------------------------
+const args = process.argv.slice(2);
+const opt = { status: 'ACTIVE', vendors: [], limit: null, priceCol: 'price', out: null, order: 'vendor' };
+for (let i = 0; i < args.length; i++) {
+ const a = args[i];
+ if (a === '--status') opt.status = args[++i];
+ else if (a === '--vendor') opt.vendors.push(args[++i]);
+ else if (a === '--limit') opt.limit = parseInt(args[++i], 10);
+ else if (a === '--price-col') opt.priceCol = args[++i];
+ else if (a === '--out') opt.out = args[++i];
+ else if (a === '--order') opt.order = args[++i];
+ else if (a === '--help' || a === '-h') {
+ console.log('Usage: node halve-preview.js [--status ACTIVE] [--vendor X]... [--limit N] [--price-col price|retail_price|min_variant_price] [--out file.csv]');
+ process.exit(0);
+ }
+}
+const allowedCols = new Set(['price', 'retail_price', 'min_variant_price']);
+if (!allowedCols.has(opt.priceCol)) {
+ console.error(`ERROR: --price-col must be one of ${[...allowedCols].join(', ')}`);
+ process.exit(2);
+}
+
+// ---- build the READ-ONLY query --------------------------------------------
+// Parameterized via psql -v to avoid injection; status + vendor list are the
+// only dynamic inputs. We pull one extra row past --limit only conceptually;
+// here we just LIMIT in SQL.
+// Single-quote-escape a SQL string literal. Inputs are trusted internal
+// vendor/status strings (CLI args from Steve/officer), never web input.
+const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
+
+const conds = [`status = ${q(opt.status)}`];
+if (opt.vendors.length) {
+ conds.push(`vendor IN (${opt.vendors.map(q).join(', ')})`);
+}
+const limitClause = opt.limit ? `LIMIT ${parseInt(opt.limit, 10)}` : '';
+
+const sql = `
+ SELECT
+ coalesce(dw_sku, sku, variant_sku, '') AS dw_sku,
+ coalesce(vendor, '') AS vendor,
+ ${opt.priceCol} AS current_retail
+ FROM shopify_products
+ WHERE ${conds.join(' AND ')}
+ ORDER BY ${opt.order === 'random' ? 'random()' : 'vendor, dw_sku'}
+ ${limitClause};
+`;
+
+const psqlArgs = ['-d', 'dw_unified', '-F', '\t', '-A', '-t', '--no-align', '-c', sql];
+
+let raw;
+try {
+ raw = execFileSync('psql', psqlArgs, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 512 });
+} catch (e) {
+ console.error('psql read failed:', e.message);
+ process.exit(1);
+}
+
+// ---- transform -------------------------------------------------------------
+function round2(n) { return Math.round((n + Number.EPSILON) * 100) / 100; }
+function csvField(s) {
+ s = (s === null || s === undefined) ? '' : String(s);
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
+}
+
+const rows = raw.split('\n').filter((l) => l.length > 0);
+const out = [['dw_sku', 'vendor', 'current_retail', 'halved_retail', 'sample_price', 'excluded_reason']];
+let nEligible = 0, nMap = 0, nZero = 0;
+
+for (const line of rows) {
+ const [dw_sku, vendor, currentRaw] = line.split('\t');
+ const current = currentRaw === '' || currentRaw === undefined ? null : Number(currentRaw);
+ let halved = '';
+ let reason = '';
+
+ if (isMapVendor(vendor)) {
+ reason = 'MAP';
+ nMap++;
+ } else if (current === null || !isFinite(current) || current <= 0 || round2(current / 2) <= 0) {
+ reason = 'zero_price_guard';
+ nZero++;
+ } else {
+ halved = round2(current / 2).toFixed(2);
+ nEligible++;
+ }
+
+ out.push([
+ dw_sku,
+ vendor,
+ current === null || !isFinite(current) ? '' : current.toFixed(2),
+ halved,
+ SAMPLE_PRICE,
+ reason,
+ ]);
+}
+
+const csv = out.map((r) => r.map(csvField).join(',')).join('\n') + '\n';
+if (opt.out) {
+ fs.writeFileSync(opt.out, csv);
+ console.error(`Wrote ${rows.length} rows -> ${opt.out}`);
+} else {
+ process.stdout.write(csv);
+}
+
+// ---- summary to stderr (so stdout stays a clean CSV when piped) -----------
+console.error('--- summary ---');
+console.error(`price-col (current_retail source): ${opt.priceCol}`);
+console.error(`status scope: ${opt.status}` + (opt.vendors.length ? ` | vendors: ${opt.vendors.join(', ')}` : ' | all vendors') + (opt.limit ? ` | limit ${opt.limit}` : ''));
+console.error(`total rows: ${rows.length}`);
+console.error(`eligible: ${nEligible}`);
+console.error(`excluded MAP: ${nMap}`);
+console.error(`zero_price_guard: ${nZero}`);
+console.error('cost: $0 (local)');
(oldest)
·
back to Dw Retail Halving Preview
·
(newest)