[object Object]

← back to Dw Validator Debug TK11314

TK-10046: harden Bespoke reassign applier per contrarian gate

dbe8e102ac5ebe1f64aef861c81e167d6f135d42 · 2026-08-11 07:18:58 -0700 · Steve Abrams

Contrarian (FIX FIRST) found 1 BLOCKER + 3 MAJORs, all fixed + re-verified:
- BLOCKER: within-product new_sku uniqueness Set check -> fail-skip to manual (caught a
  REAL case: infinite-sizeA-hudson has 2 variants collapsing to the same new SKU).
- MAJOR: PG rollback on Shopify failure (mirror never drifts ahead of the store).
- MAJOR: blank-SKU variants logged (results.skippedBlank) not silently dropped.
- MAJOR: post-mint live-Shopify base check (baseLiveOnShopify, fail-closed) guards against
  dw_sku_registry-mirror staleness vs Kamatera-canonical shopify_products.
Full dry-run post-fix: 37 reassign clean, 1 base-less deferred (fiona), 1 internal-dup
deferred (infinite-size hudson), 0 blank, 0 malformed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit dbe8e102ac5ebe1f64aef861c81e167d6f135d42
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 11 07:18:58 2026 -0700

    TK-10046: harden Bespoke reassign applier per contrarian gate
    
    Contrarian (FIX FIRST) found 1 BLOCKER + 3 MAJORs, all fixed + re-verified:
    - BLOCKER: within-product new_sku uniqueness Set check -> fail-skip to manual (caught a
      REAL case: infinite-sizeA-hudson has 2 variants collapsing to the same new SKU).
    - MAJOR: PG rollback on Shopify failure (mirror never drifts ahead of the store).
    - MAJOR: blank-SKU variants logged (results.skippedBlank) not silently dropped.
    - MAJOR: post-mint live-Shopify base check (baseLiveOnShopify, fail-closed) guards against
      dw_sku_registry-mirror staleness vs Kamatera-canonical shopify_products.
    Full dry-run post-fix: 37 reassign clean, 1 base-less deferred (fiona), 1 internal-dup
    deferred (infinite-size hudson), 0 blank, 0 malformed.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .../tk10046-bespoke-reassign.js                    | 41 +++++++++++++++++-----
 1 file changed, 33 insertions(+), 8 deletions(-)

diff --git a/scripts/tk10002-null-sample/tk10046-bespoke-reassign.js b/scripts/tk10002-null-sample/tk10046-bespoke-reassign.js
index fda999ee..9101dca2 100644
--- a/scripts/tk10002-null-sample/tk10046-bespoke-reassign.js
+++ b/scripts/tk10002-null-sample/tk10046-bespoke-reassign.js
@@ -76,6 +76,15 @@ async function gql(query, variables) {
 const MUT = `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){
   productVariantsBulkUpdate(productId:$productId,variants:$variants){ productVariants{ id inventoryItem{ sku } } userErrors{ field message } } }`;
 
+// Live Shopify check: is `base` (or any `base-*` variant) already live on an ACTIVE product?
+// Belt-and-suspenders against dw_sku_registry-mirror staleness (Kamatera owns shopify_products).
+async function baseLiveOnShopify(base) {
+  const q = `{ a: productVariants(first:1, query:"sku:'${base}'"){ nodes{ id } }
+              b: productVariants(first:1, query:"sku:${base}-*"){ nodes{ id } } }`;
+  try { const d = await gql(q); return (d.a.nodes.length + d.b.nodes.length) > 0; }
+  catch (_) { return true; } // fail CLOSED: on query error treat as taken, mint another
+}
+
 // Reserve a fresh, unique DIGAI base atomically. DRY-RUN returns a deterministic placeholder.
 async function mintBase(seqStart) {
   if (!APPLY) return { base: `<MINT@apply>`, seq: seqStart };
@@ -85,8 +94,9 @@ async function mintBase(seqStart) {
   const yy = String(now.getFullYear()).slice(-2);
   for (let seq = seqStart; seq < seqStart + 5000; seq++) {
     const base = `DIGAI-${mm}${dd}${yy}${String(seq).padStart(3, '0')}`;
-    const taken = await skuTakenOnActiveProduct(base);   // fails CLOSED on DB error
+    const taken = await skuTakenOnActiveProduct(base);   // fails CLOSED on DB error (mirror)
     if (taken.taken) continue;
+    if (await baseLiveOnShopify(base)) continue;         // fails CLOSED on live-Shopify authority
     const r = await pool.query(
       `INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku)
        VALUES ($1,'DIGAI','DW Bespoke Studios',$1) ON CONFLICT (dw_sku) DO NOTHING`, [base]);
@@ -109,7 +119,7 @@ async function main() {
   for (const d of deferred) console.log(`  DEFER ${d.handle} (${d.variant_count} vars, ${d.baseless.length} base-less) → manual review`);
   console.log('');
 
-  const results = { applied: [], skipped: [], failed: [], deferred: deferred.map(d => d.handle) };
+  const results = { applied: [], skipped: [], failed: [], skippedBlank: [], deferred: deferred.map(d => d.handle) };
   let seqStart = 1, count = 0;
 
   for (const p of eligible) {
@@ -120,7 +130,11 @@ async function main() {
     const prod = d.product;
     if (!prod) { results.skipped.push({ handle: p.handle, reason: 'product not found live' }); console.log('⚠', p.handle, 'not found live — skip'); continue; }
 
-    const live = (prod.variants.nodes || []).map(v => ({ id: v.id, sku: v.inventoryItem?.sku || '' })).filter(v => v.sku);
+    const allNodes = prod.variants.nodes || [];
+    // Blank-SKU variants can't be safely reassigned and must NOT vanish from the audit trail.
+    const blank = allNodes.filter(v => !(v.inventoryItem?.sku)).map(v => v.id);
+    if (blank.length) { results.skippedBlank.push({ handle: p.handle, variantIds: blank }); console.log('⚠', p.handle, `${blank.length} blank-SKU variant(s) — left untouched, flagged`); }
+    const live = allNodes.map(v => ({ id: v.id, sku: v.inventoryItem?.sku || '' })).filter(v => v.sku);
     // Only move variants that still carry a DIG-family base (already-reassigned ones are skipped).
     const movable = live.filter(v => BASE_RE.test(v.sku.trim()));
     if (!movable.length) { results.skipped.push({ handle: p.handle, reason: 'no DIG-base variants live (already reassigned?)' }); console.log('=', p.handle, 'already clean — skip'); continue; }
@@ -129,6 +143,13 @@ async function main() {
     const moves = movable.map(v => ({ id: v.id, old: v.sku, neu: transform(v.sku, mint.base) }))
                          .filter(m => m.neu && m.neu !== m.old);
     if (!moves.length) { results.skipped.push({ handle: p.handle, reason: 'transform produced no change' }); continue; }
+    // BLOCKER guard: two variants must never collapse to the SAME new SKU (within-product collision).
+    const neuSet = new Set(moves.map(m => m.neu));
+    if (neuSet.size !== moves.length) {
+      const dupes = moves.map(m => m.neu).filter((n, i, a) => a.indexOf(n) !== i);
+      results.failed.push({ handle: p.handle, err: 'within-product dup new_sku: ' + [...new Set(dupes)].join(', ') });
+      console.log('✗', p.handle, 'WITHIN-PRODUCT DUP new_sku — SKIP (manual):', [...new Set(dupes)].join(', ')); continue;
+    }
 
     if (!APPLY) {
       console.log(`[dry] ${p.handle} [${prod.status}] base=${mint.base} · ${moves.length} moves` +
@@ -137,27 +158,31 @@ async function main() {
       continue;
     }
 
-    // PG mirror first, then Shopify (authoritative), per variant.
+    // PG mirror first, then Shopify (authoritative). If Shopify fails, roll PG back so the
+    // mirror never drifts ahead of the store (Shopify is the source of truth).
+    const rollbackPg = async () => { for (const m of moves) { try { await pool.query(
+        `UPDATE shopify_products SET variant_sku=$1, synced_at=NOW() WHERE split_part(shopify_id,'/',5)=$2 AND variant_sku=$3`,
+        [m.old, p.product_id, m.neu]); } catch (_) {} } };
     try {
       for (const m of moves) {
         await pool.query(
           `UPDATE shopify_products SET variant_sku=$1, synced_at=NOW() WHERE split_part(shopify_id,'/',5)=$2 AND variant_sku=$3`,
           [m.neu, p.product_id, m.old]);
       }
-    } catch (e) { results.failed.push({ handle: p.handle, err: 'pg ' + e.message }); console.log('✗', p.handle, 'pg', e.message); continue; }
+    } catch (e) { await rollbackPg(); results.failed.push({ handle: p.handle, err: 'pg ' + e.message }); console.log('✗', p.handle, 'pg (rolled back)', e.message); continue; }
     try {
       const r = await gql(MUT, { productId: gid, variants: moves.map(m => ({ id: m.id, inventoryItem: { sku: m.neu } })) });
       const ue = r.productVariantsBulkUpdate.userErrors;
-      if (ue.length) { results.failed.push({ handle: p.handle, err: 'userErrors ' + JSON.stringify(ue) }); console.log('✗', p.handle, 'ue', JSON.stringify(ue)); continue; }
+      if (ue.length) { await rollbackPg(); results.failed.push({ handle: p.handle, err: 'userErrors ' + JSON.stringify(ue) }); console.log('✗', p.handle, 'ue (pg rolled back)', JSON.stringify(ue)); continue; }
       results.applied.push({ handle: p.handle, base: mint.base, moves: moves.length });
       console.log('✓', p.handle, '->', mint.base, `(${moves.length} variants)`);
-    } catch (e) { results.failed.push({ handle: p.handle, err: 'shopify ' + e.message }); console.log('✗', p.handle, 'shopify', e.message); }
+    } catch (e) { await rollbackPg(); results.failed.push({ handle: p.handle, err: 'shopify ' + e.message }); console.log('✗', p.handle, 'shopify (pg rolled back)', e.message); }
     await sleep(400);
   }
 
   const outDir = path.join(process.env.HOME, 'Projects/ticket-system/tk10002-phase2b');
   fs.writeFileSync(path.join(outDir, 'tk10046-bespoke-reassign-results.json'), JSON.stringify(results, null, 2));
-  console.log(`\nDONE: applied/planned=${results.applied.length} skipped=${results.skipped.length} failed=${results.failed.length} deferred=${results.deferred.length}`);
+  console.log(`\nDONE: applied/planned=${results.applied.length} skipped=${results.skipped.length} failed=${results.failed.length} skippedBlank=${results.skippedBlank.length} deferred=${results.deferred.length}`);
   if (!APPLY) console.log('(DRY-RUN — set APPLY=1 with an approved artifact for scope "bespoke-collision-reassign" to write)');
   await pool.end();
 }

← 1c086e8f TK-10046: Bespoke variant-level reassign applier (dry-run de  ·  back to Dw Validator Debug TK11314  ·  auto-data-snapshot: 2026-08-11T09:10:52 (1 data files) — sho 8cb7715f →