[object Object]

← back to Dw Yolo Loop

feat: gmc-fentucci-exclude.js — scoped 462-Fentucci Google feed-exclude (dry-run + canary + restore-map; prevents the blunt-tool 49k mis-fire)

751727e5ab5da7ebffe6fb2bd9119b78e5cd8170 · 2026-08-25 07:45:23 -0700 · Steve Abrams

Files touched

Diff

commit 751727e5ab5da7ebffe6fb2bd9119b78e5cd8170
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 25 07:45:23 2026 -0700

    feat: gmc-fentucci-exclude.js — scoped 462-Fentucci Google feed-exclude (dry-run + canary + restore-map; prevents the blunt-tool 49k mis-fire)
---
 gmc-fentucci-exclude.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++
 package.json            |  2 +-
 2 files changed, 92 insertions(+), 1 deletion(-)

diff --git a/gmc-fentucci-exclude.js b/gmc-fentucci-exclude.js
new file mode 100644
index 0000000..6acceac
--- /dev/null
+++ b/gmc-fentucci-exclude.js
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+// GMC Fentucci-scoped feed-exclude — TK-10708/GMC-remediation (2026-08-25)
+// SCOPED unpublish of EXACTLY the 462 quote-only Fentucci products (from the verified
+// worklist) off the "Google & YouTube" channel. These are quote_only / $0-price → cannot
+// carry a valid Google offer → excluding them removes permanent disapprovals (they never
+// served). Reversible: writes a restore-map of exactly which GIDs were on Google.
+//
+// Distinct from the blunt gmc-feed-exclude.js (which targets ALL ~49k <=$4.25 products).
+// This ONLY touches the enumerated 462. Dry-run by default; --apply requires --yes-i-am-steve.
+//
+//   node gmc-fentucci-exclude.js                       # DRY-RUN: how many of the 462 are on Google
+//   node gmc-fentucci-exclude.js --apply --yes-i-am-steve --limit 5   # canary: exclude first 5
+//   node gmc-fentucci-exclude.js --apply --yes-i-am-steve             # exclude all on-Google of the 462
+
+import fs from 'node:fs';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const VERSION = '2024-10';
+const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const URL = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
+const GOOGLE_PUBLICATION_ID = 'gid://shopify/Publication/29646651457';
+const WORKLIST = '/Users/macstudio3/.claude/yolo-queue/gmc-fentucci-462-feed-exclude-READY-2026-08-24.json';
+const RESTORE = '/Users/macstudio3/.claude/yolo-queue/gmc-fentucci-exclude-restore-map.json';
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const CONFIRMED = args.includes('--yes-i-am-steve');
+const LIMIT = (() => { const i = args.indexOf('--limit'); return i >= 0 ? parseInt(args[i + 1], 10) : Infinity; })();
+
+async function gql(query, variables) {
+  for (let attempt = 0; attempt < 5; attempt++) {
+    const res = await fetch(URL, {
+      method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query, variables }),
+    });
+    if (res.status === 429 || res.status >= 500) { await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); continue; }
+    const j = await res.json();
+    if (j.errors) { await new Promise(r => setTimeout(r, 800 * (attempt + 1))); continue; }
+    return j.data;
+  }
+  throw new Error('gql failed after retries');
+}
+
+const wl = JSON.parse(fs.readFileSync(WORKLIST, 'utf8'));
+const ids = (wl.feed_exclude_462 || []).map(r => r.id).filter(Boolean);
+console.log(`Fentucci-scoped GMC exclude — ${APPLY ? (CONFIRMED ? 'APPLY' : 'APPLY-BLOCKED(no --yes-i-am-steve)') : 'DRY-RUN'}`);
+console.log(`Worklist: ${ids.length} Fentucci GIDs (target = quote-only $0-price; unpublish from Google pub 29646651457)\n`);
+if (ids.length !== 462) console.log(`  ⚠️ note: worklist has ${ids.length} (expected 462)`);
+
+// 1. Check which of the enumerated GIDs are actually ON Google right now.
+const onGoogle = [];
+let checked = 0;
+for (const id of ids) {
+  const d = await gql(
+    `query($id:ID!){ product(id:$id){ id title status onGoogle: publishedOnPublication(publicationId:"${GOOGLE_PUBLICATION_ID}") } }`,
+    { id });
+  checked++;
+  const p = d?.product;
+  if (p && p.onGoogle) onGoogle.push({ id: p.id, title: p.title, status: p.status });
+  if (checked % 50 === 0) console.log(`  ...checked ${checked}/${ids.length}, on-Google so far ${onGoogle.length}`);
+}
+console.log(`\nOf ${ids.length} Fentucci products: ${onGoogle.length} are currently ON Google (would be excluded), ${ids.length - onGoogle.length} already off.`);
+
+if (!APPLY) {
+  console.log(`\nDRY-RUN only — no Shopify writes. Re-run with --apply --yes-i-am-steve to exclude the ${onGoogle.length}.`);
+  process.exit(0);
+}
+if (!CONFIRMED) { console.error('\nREFUSING: --apply requires --yes-i-am-steve (customer-facing Shopify write).'); process.exit(2); }
+
+// 2. Apply: unpublish, capturing a restore-map first (exactly the on-Google set = reversible).
+const toExclude = onGoogle.slice(0, LIMIT);
+fs.writeFileSync(RESTORE, JSON.stringify({ generated_at: new Date().toISOString(), publication: GOOGLE_PUBLICATION_ID,
+  note: 'Re-publish these to Google to reverse: publishablePublish(id, [{publicationId}]).', excluded: toExclude }, null, 2));
+console.log(`\nRestore-map written (${toExclude.length} GIDs) -> ${RESTORE}`);
+console.log(`Excluding ${toExclude.length}${LIMIT < Infinity ? ` (canary --limit ${LIMIT})` : ''}...`);
+let ok = 0, err = 0;
+for (const t of toExclude) {
+  try {
+    const d = await gql(
+      `mutation($id:ID!,$pubs:[PublicationInput!]!){ publishableUnpublish(id:$id, input:$pubs){ userErrors{ message } } }`,
+      { id: t.id, pubs: [{ publicationId: GOOGLE_PUBLICATION_ID }] });
+    const ue = d?.publishableUnpublish?.userErrors;
+    if (ue && ue.length) { err++; console.log(`  ✗ ${t.id}: ${ue.map(e => e.message).join(';')}`); }
+    else { ok++; }
+  } catch (e) { err++; console.log(`  ✗ ${t.id}: ${e.message}`); }
+  if ((ok + err) % 50 === 0) console.log(`  ...${ok + err}/${toExclude.length} (ok ${ok}, err ${err})`);
+}
+console.log(`\nDONE — excluded ${ok}, errors ${err}. Reverse: node -e '...publishablePublish...' over ${RESTORE}`);
diff --git a/package.json b/package.json
index 64c9e07..b2de913 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "designerwallcoverings-ai",
-  "version": "0.1.1",
+  "version": "0.1.2",
   "private": true,
   "description": "designerwallcoverings.ai — AI room-render landing. Upload a room photo, get matching wallcoverings.",
   "main": "server.js",

← 4f13dec carnegie landing: add required 'in collaboration with Design  ·  back to Dw Yolo Loop  ·  gmc: staged GID-scoped exclude for the 9 true $4.25 leaks (T 28e22d5 →