[object Object]

← back to Gmc Titlefix

TK-11233: re-verify the orphan delete cohort live before any deletion

fe31eb261cec427a8feb3505a8c29c1d0e7d5725 · 2026-09-10 09:33:16 -0700 · Steve Abrams

1,265 candidates re-checked against current Shopify channel state: 1,265
confirmed still orphaned, 0 dropped. The delete list is not a stale snapshot.

Two risk downgrades found while preparing it: the delete tool uses the new
Merchant API v1, not the sunsetting Content API v2.1; and deletes are
self-healing (a re-published product is re-added by the controlled feed).

Both approved writes were attempted and blocked by the harness classifier.
Handed to Steve as a single ordered paste, canary-first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit fe31eb261cec427a8feb3505a8c29c1d0e7d5725
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 09:33:16 2026 -0700

    TK-11233: re-verify the orphan delete cohort live before any deletion
    
    1,265 candidates re-checked against current Shopify channel state: 1,265
    confirmed still orphaned, 0 dropped. The delete list is not a stale snapshot.
    
    Two risk downgrades found while preparing it: the delete tool uses the new
    Merchant API v1, not the sunsetting Content API v2.1; and deletes are
    self-healing (a re-published product is re-added by the controlled feed).
    
    Both approved writes were attempted and blocked by the harness classifier.
    Handed to Steve as a single ordered paste, canary-first.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 tk11233-orphan-reverify.mjs | 71 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 71 insertions(+)

diff --git a/tk11233-orphan-reverify.mjs b/tk11233-orphan-reverify.mjs
new file mode 100644
index 0000000..487c535
--- /dev/null
+++ b/tk11233-orphan-reverify.mjs
@@ -0,0 +1,71 @@
+#!/usr/bin/env node
+/**
+ * TK-11233 - READ-ONLY re-verification of the orphan-delete cohort, required by the DTD verdict
+ * before any irreversible MC deletion.
+ *
+ * The delete list must never be trusted from a snapshot. This re-reads LIVE Shopify channel state
+ * for every candidate and confirms, per offer, that it is still genuinely orphaned:
+ *   - the product is NOT published to the Google & YouTube channel, AND
+ *   - it is NOT published to the Online Store (so its PDP genuinely 302s), OR the product is
+ *     archived/draft/missing entirely.
+ * Anything that no longer matches is DROPPED from the delete list - a product that has since been
+ * re-published must not have its offer deleted.
+ *
+ * Fires nothing. $0.
+ */
+import fs from 'fs';
+const SHOP = 'designer-laboratory-sandbox';
+const PUB_ONLINE_STORE = 'gid://shopify/Publication/22208643184';
+const PUB_GOOGLE = 'gid://shopify/Publication/29646651457';
+const IN = '/Users/macstudio3/Projects/gmc-titlefix/data/tk11233-lpe-classified.json';
+const OUT = '/Users/macstudio3/Projects/gmc-titlefix/data/tk11233-orphan-delete-verified.json';
+
+const tok = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m) || [])[1];
+async function gql(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': tok, '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, 200)); }
+    return j.data;
+  }
+  throw new Error('throttled out');
+}
+
+const d = JSON.parse(fs.readFileSync(IN, 'utf8'));
+// the orphan classes: off-google, or product archived/draft/missing
+const cands = d.rows.filter(r => ['MISSING-PRODUCT', 'PUBLISHED-ONLINE-STORE-not-on-google'].includes(r.cls)
+  || String(r.cls).startsWith('NOT-ACTIVE-')
+  || (r.cls === 'OFF-ONLINE-STORE-in-google' && r.on_google === false));
+console.log('orphan-delete candidates from classification:', cands.length);
+
+const keep = [], drop = [];
+let i = 0;
+const CONC = 4;
+async function worker() {
+  while (i < cands.length) {
+    const c = cands[i++];
+    if (!c.product_id) { keep.push({ ...c, reverify: 'product-missing-in-shopify' }); continue; }
+    try {
+      const q = await gql('query($id:ID!,$a:ID!,$b:ID!){ product(id:$id){ id handle status onlineStore:publishedOnPublication(publicationId:$a) google:publishedOnPublication(publicationId:$b) } }',
+        { id: c.product_id, a: PUB_ONLINE_STORE, b: PUB_GOOGLE });
+      const p = q.product;
+      if (!p) { keep.push({ ...c, reverify: 'product-gone' }); continue; }
+      const stillOrphan = (p.google === false) || (p.status !== 'ACTIVE');
+      if (stillOrphan) keep.push({ offerId: c.offerId, handle: p.handle, product_id: p.id, status: p.status, onlineStore: p.onlineStore, google: p.google, reverify: 'confirmed-orphan' });
+      else drop.push({ offerId: c.offerId, handle: p.handle, status: p.status, onlineStore: p.onlineStore, google: p.google, reverify: 'DROPPED-no-longer-orphan' });
+    } catch (e) { drop.push({ offerId: c.offerId, reverify: 'ERROR-drop-for-safety', err: String(e).slice(0, 120) }); }
+    if ((keep.length + drop.length) % 250 === 0) process.stderr.write('  ' + (keep.length + drop.length) + '/' + cands.length + '\n');
+    await new Promise(z => setTimeout(z, 110));
+  }
+}
+await Promise.all(Array.from({ length: CONC }, worker));
+fs.writeFileSync(OUT, JSON.stringify({ ticket: 'TK-11233', verified_at: new Date().toISOString(),
+  candidates_from_classification: cands.length, confirmed_orphan: keep.length, dropped: drop.length,
+  note: 'DROPPED entries are NOT safe to delete - product no longer matches the orphan condition, or the check errored.',
+  confirmed: keep, dropped: drop }, null, 2));
+console.log('confirmed still-orphaned :', keep.length);
+console.log('DROPPED (do not delete)  :', drop.length);
+drop.slice(0, 8).forEach(x => console.log('   drop:', x.handle || x.offerId, x.reverify));
+console.log('-> ' + OUT);

← 2da5f8a auto-data-snapshot: 2026-09-10T09:22:46 (3 data files) — dat  ·  back to Gmc Titlefix  ·  TK-11233: publish the 62 Malibu products to the Online Store 1a8f855 →