[object Object]

← 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

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 →