← back to Sanderson Onboard
chore: lint (1 dead-var removed) + refactor (named consts, toRestoreRows helper, revert-map guards — behavior-preserving), v1.1.2 → v1.2.0 (session close)
494134f98e03b9fdabb28f4085c6e6aebabb36b8 · 2026-08-31 09:53:08 -0700 · Steve Abrams
Files touched
M tk10873/VERSIONM tk10873/apply_labeling.mjsM tk10873/retire_dupes.mjs
Diff
commit 494134f98e03b9fdabb28f4085c6e6aebabb36b8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 09:53:08 2026 -0700
chore: lint (1 dead-var removed) + refactor (named consts, toRestoreRows helper, revert-map guards — behavior-preserving), v1.1.2 → v1.2.0 (session close)
---
tk10873/VERSION | 2 +-
tk10873/apply_labeling.mjs | 18 +++++++++++++-----
tk10873/retire_dupes.mjs | 22 ++++++++++++++++------
3 files changed, 30 insertions(+), 12 deletions(-)
diff --git a/tk10873/VERSION b/tk10873/VERSION
index 45a1b3f..26aaba0 100644
--- a/tk10873/VERSION
+++ b/tk10873/VERSION
@@ -1 +1 @@
-1.1.2
+1.2.0
diff --git a/tk10873/apply_labeling.mjs b/tk10873/apply_labeling.mjs
index 28e5c69..48de9d1 100644
--- a/tk10873/apply_labeling.mjs
+++ b/tk10873/apply_labeling.mjs
@@ -50,7 +50,6 @@ import { execFileSync } from 'node:child_process';
const DIR = new URL('.', import.meta.url).pathname;
const SPEC_IN = process.env.LABELING_SPEC ? `${DIR}${process.env.LABELING_SPEC}` : `${DIR}zoffany_widewidth_labeling_spec.json`;
-const BASE = SPEC_IN.replace(/\.json$/, '').split('/').pop();
const PLAN_OUT = `${DIR}apply_labeling.live_plan.json`;
const PREVIEW_OUT = `${DIR}apply_labeling.restore_map.preview.json`;
const RESTORE_OUT = `${DIR}apply_labeling.restore_map.live.json`; // populated ONLY by --apply, before writes
@@ -63,6 +62,14 @@ const SAMPLE_PRICE = 4.25;
const NEW_VALUE = 'Per Yard';
const OLD_VALUE = 'Default Title';
+// GraphQL query width limits (must cover all real products; bump if Shopify returns truncation warnings)
+const OPTIONS_FIRST = 5;
+const VARIANTS_FIRST = 20;
+
+// Throttle pacing: pause when cost bucket is low
+const THROTTLE_MIN_AVAILABLE = 200;
+const THROTTLE_PAUSE_MS = 1200;
+
const argv = process.argv.slice(2);
const APPLY = argv.includes('--apply');
const LIVE_OK = argv.includes('--i-understand-this-is-live');
@@ -113,7 +120,7 @@ async function gql(query, variables) {
if (j.errors) throw new Error('GraphQL errors: ' + JSON.stringify(j.errors));
// pace against the throttle: if we're low on the cost bucket, breathe.
const throttle = j.extensions?.cost?.throttleStatus;
- if (throttle && throttle.currentlyAvailable < 200) await new Promise(r => setTimeout(r, 1200));
+ if (throttle && throttle.currentlyAvailable < THROTTLE_MIN_AVAILABLE) await new Promise(r => setTimeout(r, THROTTLE_PAUSE_MS));
return j.data;
}
@@ -121,8 +128,8 @@ async function gql(query, variables) {
const PRODUCT_Q = `query($id: ID!) {
product(id: $id) {
id status title descriptionHtml
- options(first: 5) { id name position optionValues { id name } }
- variants(first: 20) { nodes { id sku title price selectedOptions { name value } } }
+ options(first: ${OPTIONS_FIRST}) { id name position optionValues { id name } }
+ variants(first: ${VARIANTS_FIRST}) { nodes { id sku title price selectedOptions { name value } } }
}
}`;
@@ -172,7 +179,7 @@ function appendPdpLine(oldHtml, pdpLine) {
const OPTION_UPDATE_M = `mutation($productId: ID!, $option: OptionUpdateInput!, $optionValuesToUpdate: [OptionValueUpdateInput!]) {
productOptionUpdate(productId: $productId, option: $option, optionValuesToUpdate: $optionValuesToUpdate) {
- product { id options(first: 5) { id name optionValues { id name } } }
+ product { id options(first: ${OPTIONS_FIRST}) { id name optionValues { id name } } }
userErrors { field message }
}
}`;
@@ -393,6 +400,7 @@ async function runRevert() {
// ── dispatch ──────────────────────────────────────────────────────────────────
(async () => {
+ if (REVERT_IDX >= 0 && !REVERT_MAP) throw new Error('--revert requires a restore-map path argument');
if (REVERT_MAP) return runRevert();
if (APPLY) return runApply();
return runPlan();
diff --git a/tk10873/retire_dupes.mjs b/tk10873/retire_dupes.mjs
index 87cf6ef..3613f44 100644
--- a/tk10873/retire_dupes.mjs
+++ b/tk10873/retire_dupes.mjs
@@ -24,9 +24,12 @@ const ORPHANS = [
];
const RETIRE_TO = 'DRAFT'; // reversible; restore sets back to ACTIVE
+const MAX_RETRIES = 5;
+const RETRY_BACKOFF_MS = 1200;
+
const args = process.argv.slice(2);
const APPLY = args.includes('--apply');
-const REVERT_I = args.indexOf('--revert');
+const REVERT_IDX = args.indexOf('--revert');
const LIVE_OK = args.includes('--i-understand-this-is-live');
function token() {
@@ -42,7 +45,7 @@ async function gql(query, variables) {
headers: { 'X-Shopify-Access-Token': token(), 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
- if (res.status === 429 && attempt++ < 5) { await new Promise(r => setTimeout(r, 1200 * attempt)); continue; }
+ if (res.status === 429 && attempt++ < MAX_RETRIES) { await new Promise(r => setTimeout(r, RETRY_BACKOFF_MS * attempt)); continue; }
const j = await res.json();
if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors));
return j.data;
@@ -57,6 +60,9 @@ async function resolveOne(sku) {
if (nodes.length !== 1) throw new Error(`FAIL-CLOSED: sku ${sku} resolved to ${nodes.length} products (need exactly 1)`);
return nodes[0];
}
+function toRestoreRows(resolved) {
+ return resolved.map(r => ({ sku: r.sku, product_id: r.product_id, old_status: r.old_status, new_status: RETIRE_TO }));
+}
function persist(path, obj) {
const fd = fs.openSync(path, 'w');
fs.writeSync(fd, JSON.stringify(obj, null, 2));
@@ -65,11 +71,15 @@ function persist(path, obj) {
}
// ── REVERT ────────────────────────────────────────────────────────────────────
-if (REVERT_I !== -1) {
- const mapPath = args[REVERT_I + 1];
+if (REVERT_IDX !== -1) {
+ const mapPath = args[REVERT_IDX + 1];
if (!mapPath) throw new Error('--revert needs a restore-map path');
if (!LIVE_OK) throw new Error('revert is LIVE — pass --i-understand-this-is-live');
const map = JSON.parse(fs.readFileSync(mapPath, 'utf8')).rows;
+ if (!Array.isArray(map) || !map.length) throw new Error(`restore map has no rows: ${mapPath}`);
+ for (const r of map) {
+ if (!r.product_id || !r.sku || !r.old_status) throw new Error(`restore map row missing required field (product_id/sku/old_status): ${JSON.stringify(r)}`);
+ }
let ok = 0, errs = [];
for (const r of map) {
try {
@@ -97,7 +107,7 @@ if (!APPLY) {
console.log('=== retire_dupes — PLAN (DRY-RUN, no writes) ===');
console.log(`orphans resolved: ${resolved.length}/${ORPHANS.length}`);
for (const r of resolved) console.log(` ${r.sku} "${r.title}" ${r.old_status} → ${RETIRE_TO} (${r.label})`);
- persist(PREVIEW_OUT, { generated: 'preview', retire_to: RETIRE_TO, rows: resolved.map(r => ({ sku: r.sku, product_id: r.product_id, old_status: r.old_status, new_status: RETIRE_TO })) });
+ persist(PREVIEW_OUT, { generated: 'preview', retire_to: RETIRE_TO, rows: toRestoreRows(resolved) });
console.log(`\npreview restore map → ${PREVIEW_OUT}`);
console.log('LIVE run: node retire_dupes.mjs --apply --i-understand-this-is-live (Steve, token sourced)');
process.exit(0);
@@ -105,7 +115,7 @@ if (!APPLY) {
// ── APPLY (LIVE — restore-map FIRST) ───────────────────────────────────────────
if (!LIVE_OK) throw new Error('--apply is LIVE — pass --i-understand-this-is-live');
-const restore = { generated: 'live', retire_to: RETIRE_TO, rows: resolved.map(r => ({ sku: r.sku, product_id: r.product_id, old_status: r.old_status, new_status: RETIRE_TO })) };
+const restore = { generated: 'live', retire_to: RETIRE_TO, rows: toRestoreRows(resolved) };
persist(RESTORE_OUT, restore); // COMPLETE restore map + fsync BEFORE any write — this IS the undo
console.log(`[apply] restore map persisted BEFORE any write → ${RESTORE_OUT}`);
console.log(`[apply] undo: node retire_dupes.mjs --revert ${RESTORE_OUT} --i-understand-this-is-live`);
← 2d6ad38 TK-10873: record live labeling restore map (44 rows) — the s
·
back to Sanderson Onboard
·
auto-data-snapshot: 2026-08-31T10:06:07 (3 data files) — tk1 79bbeca →