← back to Designerwallcoverings
Add scoped orphan drafter (draft only reviewed IDs, dry-run default) for the 72-orphan review
f39ee23af5227e8a68b033736db63086afa50f1f · 2026-08-06 12:32:15 -0700 · Steve Abrams
Files touched
A scripts/draft-selected-orphans.mjs
Diff
commit f39ee23af5227e8a68b033736db63086afa50f1f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 6 12:32:15 2026 -0700
Add scoped orphan drafter (draft only reviewed IDs, dry-run default) for the 72-orphan review
---
scripts/draft-selected-orphans.mjs | 50 ++++++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/scripts/draft-selected-orphans.mjs b/scripts/draft-selected-orphans.mjs
new file mode 100644
index 0000000..b80b86b
--- /dev/null
+++ b/scripts/draft-selected-orphans.mjs
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+// Scoped orphan drafter (2026-08-06) — drafts ONLY the product IDs in a given id-file, instead of the
+// full orphan-publish-guard --enforce sweep (which would also draft intentional showroom/index pages).
+// Reuses the guard's reversible mechanism: Shopify productUpdate(status:DRAFT) + append to the same
+// guard-enforced-ledger.jsonl + flip the dw_unified mirror. DRY-RUN by default; pass --apply to write.
+// node scripts/draft-selected-orphans.mjs /tmp/orphans_draft_ids.txt # dry-run (lists, no writes)
+// node scripts/draft-selected-orphans.mjs /tmp/orphans_draft_ids.txt --apply # execute (reversible)
+process.env.PATH = `/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/local/bin:${process.env.PATH || ''}`;
+import fs from 'node:fs';
+import { execFileSync } from 'node:child_process';
+
+const APPLY = process.argv.includes('--apply');
+const idFile = process.argv.find(a => a.endsWith('.txt')) || '/tmp/orphans_draft_ids.txt';
+const ids = fs.readFileSync(idFile, 'utf8').split('\n').map(s => s.trim()).filter(Boolean);
+const DIR = '/Users/macstudio3/Projects/designerwallcoverings/data/orphan-cleanup-20260716';
+const LEDGER = `${DIR}/guard-enforced-ledger.jsonl`;
+const ENV = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
+const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const gid = id => id.startsWith('gid://') ? id : `gid://shopify/Product/${id}`;
+
+console.log(`[draft-selected-orphans] ${ids.length} target ids · ${APPLY ? 'APPLY (will DRAFT)' : 'DRY-RUN (no writes)'}`);
+if (!APPLY) { for (const id of ids) console.log(` would DRAFT ${gid(id)}`); console.log('\nRe-run with --apply to draft these.'); process.exit(0); }
+
+async function gql(q, v) {
+ for (let i = 0; i < 6; i++) {
+ let res; try { res = await fetch(ENDPOINT, { 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 (res.status === 429 || res.status >= 500) { await sleep(2000 * (i + 1)); continue; }
+ let j; try { j = await res.json(); } catch { await sleep(2000 * (i + 1)); continue; }
+ if (j.errors && JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (i + 1)); continue; }
+ return j;
+ } throw new Error('exhausted');
+}
+const MUT = `mutation($id:ID!){ productUpdate(input:{id:$id, status:DRAFT}){ product{ id status } userErrors{ message } } }`;
+const ledger = fs.createWriteStream(LEDGER, { flags: 'a' });
+let ok = 0, skip = 0;
+for (const id of ids) {
+ const g = gid(id);
+ const rr = await gql(MUT, { id: g });
+ const pu = rr.data && rr.data.productUpdate;
+ const e = (pu && pu.userErrors) || [];
+ if (!pu || e.length) { skip++; ledger.write(JSON.stringify({ ts: new Date().toISOString(), gid: g, source: 'scoped-review', reason: JSON.stringify(e.length ? e : rr.errors) }) + '\n'); console.log(` SKIP ${g}: ${JSON.stringify(e.length ? e : rr.errors)}`); continue; }
+ ledger.write(JSON.stringify({ ts: new Date().toISOString(), gid: g, source: 'scoped-review', action: 'DRAFT' }) + '\n');
+ execFileSync('psql', ['host=/tmp dbname=dw_unified', '-c', `UPDATE shopify_products SET status='DRAFT', online_store_published=false, synced_at=now() WHERE shopify_id='${g}'`]);
+ ok++; console.log(` DRAFT ${g}`); await sleep(150);
+}
+ledger.end();
+console.log(`[draft-selected-orphans] done: drafted=${ok} skipped=${skip}`);
← 50f482b go-live gate: require live manufacturer_sku before ACTIVE (T
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-08-06T13:27:53 (3 data files) — dat 9825244 →