← 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
M scripts/tk10002-null-sample/tk10046-bespoke-reassign.js
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 →