[object Object]

← back to Gmc Titlefix

TK-11449: residual-3 dedup follow-on orphan check — 3 new Google orphans found, gated memo drafted

f939270bba9e23469dd519434f5d3ff76e7e90e2 · 2026-09-13 01:14:00 -0700 · Steve

Read-only check (tk11449-residual3-orphan-check.mjs) confirms the 3 products archived by the
residual-3 dedup (executed 2026-09-13 06:47Z, after the main 61-offer orphan sweep already ran)
left the identical orphan-offer gap: $195.00 advertised, lands on the survivor's $4.25 sample.
All 3 survivors already carry their own offer, so no carve-out needed. Gated memo drafted, nothing fired.

Files touched

Diff

commit f939270bba9e23469dd519434f5d3ff76e7e90e2
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Sep 13 01:14:00 2026 -0700

    TK-11449: residual-3 dedup follow-on orphan check — 3 new Google orphans found, gated memo drafted
    
    Read-only check (tk11449-residual3-orphan-check.mjs) confirms the 3 products archived by the
    residual-3 dedup (executed 2026-09-13 06:47Z, after the main 61-offer orphan sweep already ran)
    left the identical orphan-offer gap: $195.00 advertised, lands on the survivor's $4.25 sample.
    All 3 survivors already carry their own offer, so no carve-out needed. Gated memo drafted, nothing fired.
---
 data/tk11449-residual3-orphan-check.json |  58 ++++++++++++++
 tk11449-residual3-orphan-check.mjs       | 130 +++++++++++++++++++++++++++++++
 2 files changed, 188 insertions(+)

diff --git a/data/tk11449-residual3-orphan-check.json b/data/tk11449-residual3-orphan-check.json
new file mode 100644
index 0000000..3fe88a0
--- /dev/null
+++ b/data/tk11449-residual3-orphan-check.json
@@ -0,0 +1,58 @@
+{
+ "generated_at": "2026-09-13T08:11:57.435Z",
+ "ticket": "TK-11449",
+ "enumeration_complete": true,
+ "offers_scanned": 62295,
+ "orig_matches": [
+  {
+   "mc_id": "online:en:US:43605859270707",
+   "offerId": "43605859270707",
+   "pair": "pico-stripe-margot-magenta-1",
+   "title": "Pico 6\" Fentucci Stripe - Margot Magenta",
+   "price": "195.00 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-margot-magenta-1"
+  },
+  {
+   "mc_id": "online:en:US:43605859696691",
+   "offerId": "43605859696691",
+   "pair": "pico-stripe-golden-1",
+   "title": "Pipa 12\" Fentucci Stripe - Golden",
+   "price": "195.00 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-golden-1"
+  },
+  {
+   "mc_id": "online:en:US:43605859663923",
+   "offerId": "43605859663923",
+   "pair": "pico-stripe-golden-2",
+   "title": "Pano 24\" Fentucci Stripe - Golden",
+   "price": "195.00 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-golden-2"
+  }
+ ],
+ "surv_matches": [
+  {
+   "mc_id": "online:en:US:43608487526451",
+   "offerId": "43608487526451",
+   "pair": "pico-stripe-margot-magenta-1",
+   "title": "Pico 6\" Fentucci Stripe - Margot Magenta Wallcovering | DW Bespoke Studios",
+   "price": "558.77 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-margot-magenta-1-1"
+  },
+  {
+   "mc_id": "online:en:US:43672712511539",
+   "offerId": "43672712511539",
+   "pair": "pico-stripe-golden-2",
+   "title": "Pano 24\" Fentucci Stripe - Golden Wallcovering | DW Bespoke Studios",
+   "price": "558.77 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-golden-2-1?variant=43672712511539"
+  },
+  {
+   "mc_id": "online:en:US:43608500699187",
+   "offerId": "43608500699187",
+   "pair": "pico-stripe-golden-1",
+   "title": "Pipa 12\" Fentucci Stripe - Golden Wallcovering | DW Bespoke Studios",
+   "price": "558.77 USD",
+   "link": "https://www.designerwallcoverings.com/products/pico-stripe-golden-1-1"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/tk11449-residual3-orphan-check.mjs b/tk11449-residual3-orphan-check.mjs
new file mode 100644
index 0000000..29cceaa
--- /dev/null
+++ b/tk11449-residual3-orphan-check.mjs
@@ -0,0 +1,130 @@
+#!/usr/bin/env node
+// TK-11449 — READ-ONLY. Does the residual-3 dedup (executed 2026-09-13 06:47Z) leave the same
+// orphan-Google-offer gap the 67-pair dedup left? Checks: (1) do the 3 newly-archived (legacy)
+// products still carry live Merchant Center offers (would be orphans, same shape as the 61
+// already deleted), (2) do the 3 survivors already have their OWN Google offer (if not,
+// unpublishing an orphan would zero out Google presence on a create-dead channel — the exact
+// hole found and carved out on the original 67-pair run).
+import fs from 'fs';
+const HOME = process.env.HOME;
+const SHOP = 'designer-laboratory-sandbox';
+const MERCHANT = '146735262';
+
+const shopTok = fs.readFileSync(HOME + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1];
+
+async function shopGql(q, v) {
+  for (let a = 0; a < 5; a++) {
+    const r = await fetch(`https://${SHOP}.myshopify.com/admin/api/2024-10/graphql.json`, {
+      method: 'POST', headers: { 'X-Shopify-Access-Token': shopTok, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query: q, variables: v }), signal: AbortSignal.timeout(60000) });
+    const j = await r.json();
+    if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED') && a < 4) { await new Promise(z => setTimeout(z, 2000 * (a + 1))); continue; } throw new Error(JSON.stringify(j.errors).slice(0, 300)); }
+    return j.data;
+  }
+  throw new Error('shopify throttled out');
+}
+
+const b64 = b => Buffer.from(b).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
+async function mcToken() {
+  const SA = JSON.parse(fs.readFileSync(HOME + '/Projects/secrets-manager/gmc-sa-146735262.json', 'utf8'));
+  const now = Math.floor(Date.now() / 1000);
+  const si = b64(JSON.stringify({ alg: 'RS256', typ: 'JWT' })) + '.' + b64(JSON.stringify({ iss: SA.client_email, scope: 'https://www.googleapis.com/auth/content', aud: 'https://oauth2.googleapis.com/token', iat: now, exp: now + 3600 }));
+  const crypto = await import('crypto');
+  const s = crypto.createSign('RSA-SHA256'); s.update(si); const jwt = si + '.' + b64(s.sign(SA.private_key));
+  const r = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt }) });
+  const j = await r.json(); if (!j.access_token) throw new Error('mc token fail: ' + JSON.stringify(j)); return j.access_token;
+}
+async function mcGetJson(url, H) {
+  for (let a = 0; a < 6; a++) {
+    try {
+      const r = await fetch(url, { headers: H, signal: AbortSignal.timeout(30000) });
+      if (r.status === 429 || r.status >= 500) { await new Promise(res => setTimeout(res, 2000 * (a + 1))); continue; }
+      return await r.json();
+    } catch (e) { await new Promise(res => setTimeout(res, 2000 * (a + 1))); }
+  }
+  throw new Error('mc get exhausted: ' + url);
+}
+function shopifyRefFor(offerId) {
+  let m = /^shopify_[A-Za-z]+_(\d+)_(\d+)$/.exec(offerId);
+  if (m) return { kind: 'product', productId: m[1], variantId: m[2] };
+  if (/^\d+$/.test(offerId)) return { kind: 'variant', variantId: offerId };
+  return null;
+}
+
+async function main() {
+  const rows = fs.readFileSync(HOME + '/Projects/gmc-titlefix/data/tk11449-residual3-RESTOREMAP-2026-09-13T06-41-33-083Z.jsonl', 'utf8')
+    .trim().split('\n').map(l => JSON.parse(l));
+  if (rows.length !== 3) throw new Error(`expected 3 residual rows, got ${rows.length}`);
+
+  // Resolve current variant ids for both orig (archived) and survivor products.
+  const allIds = rows.flatMap(r => [r.orig_id, r.surv_id]);
+  const Q1 = `query($ids:[ID!]!){ nodes(ids:$ids){ __typename ... on Product { id handle status
+      variants(first:100){ edges{ node{ id } } } } } }`;
+  const shopData = await shopGql(Q1, { ids: allIds });
+  const byGid = new Map();
+  (shopData.nodes || []).forEach((n, i) => byGid.set(allIds[i], n));
+
+  const origPidNum = new Map(), origVidNum = new Map(); // numeric -> row
+  const survPidNum = new Map(), survVidNum = new Map();
+  for (const r of rows) {
+    const on = byGid.get(r.orig_id), sn = byGid.get(r.surv_id);
+    if (!on) throw new Error(`orig ${r.orig_id} not found`);
+    if (!sn) throw new Error(`surv ${r.surv_id} not found`);
+    origPidNum.set(r.orig_id.split('/').pop(), r);
+    survPidNum.set(r.surv_id.split('/').pop(), r);
+    for (const e of on.variants.edges) origVidNum.set(e.node.id.split('/').pop(), r);
+    for (const e of sn.variants.edges) survVidNum.set(e.node.id.split('/').pop(), r);
+    console.log(`shopify state: ${r.clean} -> orig(legacy) status=${on.status} handle=${on.handle} | survivor status=${sn.status} handle=${sn.handle}`);
+  }
+
+  // Full MC enumeration (fresh).
+  const token = await mcToken();
+  const H = { Authorization: `Bearer ${token}` };
+  let pageToken = null, allOffers = [], pages = 0;
+  do {
+    const url = `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/products?pageSize=1000` + (pageToken ? `&pageToken=${pageToken}` : '');
+    const j = await mcGetJson(url, H);
+    for (const p of (j.products || [])) allOffers.push(p);
+    pageToken = j.nextPageToken || null;
+    pages++;
+  } while (pageToken);
+  console.log(`enumerated ${allOffers.length} offers across ${pages} pages (enumeration_complete=${!pageToken})`);
+
+  const origMatches = [], survMatches = [];
+  for (const p of allOffers) {
+    const offerId = p.offerId; if (!offerId) continue;
+    const ref = shopifyRefFor(offerId); if (!ref) continue;
+    const lang = p.contentLanguage || 'en', feedLabel = p.feedLabel || 'US';
+    const attrs = p.productAttributes || {};
+    let row = null, side = null;
+    if (ref.kind === 'product' && origPidNum.has(ref.productId)) { row = origPidNum.get(ref.productId); side = 'orig'; }
+    else if (ref.kind === 'variant' && origVidNum.has(ref.variantId)) { row = origVidNum.get(ref.variantId); side = 'orig'; }
+    else if (ref.kind === 'product' && survPidNum.has(ref.productId)) { row = survPidNum.get(ref.productId); side = 'surv'; }
+    else if (ref.kind === 'variant' && survVidNum.has(ref.variantId)) { row = survVidNum.get(ref.variantId); side = 'surv'; }
+    if (!row) continue;
+    const rec = {
+      mc_id: `online:${lang}:${feedLabel}:${offerId}`, offerId,
+      pair: row.clean,
+      title: attrs.title || null,
+      price: attrs.price ? `${(parseInt(attrs.price.amountMicros, 10) / 1e6).toFixed(2)} ${attrs.price.currencyCode}` : null,
+      link: attrs.link || null,
+    };
+    if (side === 'orig') origMatches.push(rec); else survMatches.push(rec);
+  }
+
+  console.log('\n=== ORPHAN CHECK (offers still pointing at the now-ARCHIVED originals) ===');
+  console.log(`orig-side live offers found: ${origMatches.length} (expect 0 if already clean, >0 = new orphans same shape as the 61 already deleted)`);
+  origMatches.forEach(m => console.log('  ORPHAN:', m.mc_id, m.pair, m.price, m.link));
+
+  console.log('\n=== SURVIVOR COVERAGE (does the kept product already have its own offer?) ===');
+  for (const r of rows) {
+    const has = survMatches.filter(m => m.pair === r.clean);
+    console.log(`  ${r.clean} survivor offers: ${has.length}`, has.map(h => `${h.mc_id} ${h.price}`).join(' | ') || '(none — would go to ZERO Google presence if its orphan were unpublished with no replacement, same carve-out class as pano-stripe-peck-drive-plum)');
+  }
+
+  fs.writeFileSync(HOME + '/Projects/gmc-titlefix/data/tk11449-residual3-orphan-check.json', JSON.stringify({
+    generated_at: new Date().toISOString(), ticket: 'TK-11449', enumeration_complete: !pageToken, offers_scanned: allOffers.length,
+    orig_matches: origMatches, surv_matches: survMatches,
+  }, null, 1));
+}
+main().catch(e => { console.error('FATAL', e); process.exit(1); });

← 8bb2326 auto-data-snapshot: 2026-09-13T00:21:19 (1 data files) — dat  ·  back to Gmc Titlefix  ·  TK-11449: read-only final verification sweep — 61 deletions 3c89606 →