← back to Designerwallcoverings
feat: carnegie-siltech-grain line-drafter (dry-run default, live-sourced, Cody-hardened) — session close
806c1cdd486aeb88c924d8be9d55665c44f97a20 · 2026-08-22 10:55:25 -0700 · Steve Abrams
Files touched
A scripts/draft-carnegie-siltech-grain.mjs
Diff
commit 806c1cdd486aeb88c924d8be9d55665c44f97a20
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 22 10:55:25 2026 -0700
feat: carnegie-siltech-grain line-drafter (dry-run default, live-sourced, Cody-hardened) — session close
---
scripts/draft-carnegie-siltech-grain.mjs | 76 ++++++++++++++++++++++++++++++++
1 file changed, 76 insertions(+)
diff --git a/scripts/draft-carnegie-siltech-grain.mjs b/scripts/draft-carnegie-siltech-grain.mjs
new file mode 100644
index 0000000..fb2eb8a
--- /dev/null
+++ b/scripts/draft-carnegie-siltech-grain.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// Draft the Carnegie siltech-grain line (2026-08-21, yoloforever Cycle 2, TK-10776).
+// Steve APPROVED "option 1: set siltech-grain to DRAFT now, fix and re-activate properly" (approve 1-4).
+//
+// SCOPE (Cody-corrected): the WHOLE live ACTIVE carnegie-siltech-grain line. Sourced from LIVE Shopify
+// (query handle:carnegie-siltech-grain*), NOT the local mirror — the mirror showed 53 ACTIVE but the
+// live 5-field canary found 83, so a mirror-sourced list would silently miss up to 30 live violators.
+// Steve's "set the line to DRAFT" = draft every ACTIVE product in the line (fix + re-activate later),
+// so we do NOT narrow to the empty-description subset (that left non-description 5-field violators live).
+// REVERSIBLE: productUpdate(status:DRAFT) flips straight back to ACTIVE; every action ledgered with undo.
+// SAFETY: GID validated against /^gid:\/\/shopify\/Product\/\d+$/ before any psql write (no interpolation
+// of an unvalidated identifier); a hard SANITY CAP aborts --apply if the live ACTIVE count is absurd.
+// DRY-RUN by default. Pass --apply to write. Customer-facing Shopify write => run via Steve's ! only.
+// node scripts/draft-carnegie-siltech-grain.mjs # dry-run (live-enumerates, lists, no writes)
+// node scripts/draft-carnegie-siltech-grain.mjs --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 CAP = 200; // sanity cap: siltech-grain line is ~83 active; abort --apply if we somehow see > CAP
+const GID_RE = /^gid:\/\/shopify\/Product\/\d+$/;
+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 LEDGER = '/Users/macstudio3/Projects/designerwallcoverings/data/carnegie-siltech-grain-draft-ledger.jsonl';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+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('gql exhausted');
+}
+
+// 1) enumerate the LIVE ACTIVE line straight from Shopify (authoritative), paginated
+const LIST = `query($cursor:String){ products(first:100, query:"handle:carnegie-siltech-grain* status:active", after:$cursor){ pageInfo{ hasNextPage endCursor } nodes{ id status title bodyHtml } } }`;
+const active = [];
+let cursor = null;
+for (let page = 0; page < 50; page++) {
+ const r = await gql(LIST, { cursor });
+ const c = r.data && r.data.products;
+ if (!c) { console.error('query failed:', JSON.stringify(r.errors || r).slice(0, 300)); process.exit(1); }
+ for (const n of c.nodes) { if (n.status === 'ACTIVE' && GID_RE.test(n.id)) active.push({ g: n.id, title: n.title, noDesc: !n.bodyHtml || n.bodyHtml.replace(/<[^>]*>/g, '').trim().length === 0 }); }
+ if (!c.pageInfo.hasNextPage) break;
+ cursor = c.pageInfo.endCursor; await sleep(120);
+}
+const noDescN = active.filter(a => a.noDesc).length;
+console.log(`[carnegie-siltech-grain] LIVE ACTIVE in line: ${active.length} (${noDescN} missing description) · ${APPLY ? 'APPLY (will DRAFT the line)' : 'DRY-RUN (no writes)'}`);
+for (const a of active) console.log(` ${APPLY ? 'DRAFT' : 'would DRAFT'} ${a.g} ${a.noDesc ? '[no-desc] ' : ''}${a.title}`);
+
+if (!APPLY) { console.log(`\nDRY-RUN — re-run with --apply to DRAFT these ${active.length} (reversible via ledger).`); process.exit(0); }
+if (active.length > CAP) { console.error(`ABORT: ${active.length} > sanity cap ${CAP}. Re-verify the query scope before drafting.`); process.exit(2); }
+if (active.length === 0) { console.log('Nothing ACTIVE in the line — already drafted/archived. No-op.'); process.exit(0); }
+
+// 2) apply: DRAFT + ledger + mirror-flip (GID pre-validated, so no unvalidated interpolation)
+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 a of active) {
+ if (!GID_RE.test(a.g)) { skip++; console.log(` SKIP malformed gid ${a.g}`); continue; }
+ const rr = await gql(MUT, { id: a.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: a.g, source: 'carnegie-siltech-grain-line', reason: JSON.stringify(e.length ? e : rr.errors) }) + '\n'); console.log(` SKIP ${a.g}: ${JSON.stringify(e.length ? e : rr.errors)}`); continue; }
+ ledger.write(JSON.stringify({ ts: new Date().toISOString(), gid: a.g, source: 'carnegie-siltech-grain-line', action: 'DRAFT', undo: `productUpdate status:ACTIVE ${a.g}` }) + '\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='${a.g}'`]); // a.g GID-format-validated above
+ ok++; console.log(` DRAFT ${a.g}`); await sleep(150);
+}
+ledger.end();
+console.log(`[carnegie-siltech-grain] done: drafted=${ok} skipped=${skip} · ledger=${LEDGER}`);
+console.log(`UNDO: for each gid in the ledger, productUpdate(status:ACTIVE).`);
← 822c9a8 auto-data-snapshot: 2026-08-22T08:08:28 (3 data files) — dat
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-08-22T14:31:07 (1 data files) — dat 3c7f8d5 →