← back to Designer Wallcoverings
Anna French STEP4+5: onboarding gate (validateBeforeActivate) + diff-vs-live audit (read-only)
30bafd58f7ebb0871d13fdf5bbcc748933a28efc · 2026-06-25 08:43:51 -0700 · Steve
Files touched
A shopify/scripts/anna-french-gate-diff.js
Diff
commit 30bafd58f7ebb0871d13fdf5bbcc748933a28efc
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 25 08:43:51 2026 -0700
Anna French STEP4+5: onboarding gate (validateBeforeActivate) + diff-vs-live audit (read-only)
---
shopify/scripts/anna-french-gate-diff.js | 134 +++++++++++++++++++++++++++++++
1 file changed, 134 insertions(+)
diff --git a/shopify/scripts/anna-french-gate-diff.js b/shopify/scripts/anna-french-gate-diff.js
new file mode 100644
index 00000000..a6ab2716
--- /dev/null
+++ b/shopify/scripts/anna-french-gate-diff.js
@@ -0,0 +1,134 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Anna French — STEP 4 (onboarding activation gate) + STEP 5 (diff vs live 1,122).
+ * READ-ONLY. No Shopify writes, no staging mutations. Emits a JSON audit + console tally.
+ *
+ * STEP 4: run validateBeforeActivate() against every enriched staging row → PASS/FAIL
+ * with the specific Needs-* reasons.
+ * STEP 5: join staging ↔ live shopify_products (vendor='Anna French') on upper(mfr_sku)
+ * and classify each: keep-as-is / re-prefix / repair-handle / gate-pass-now / still-fails-gate
+ * / discontinued-archive.
+ *
+ * Usage: node anna-french-gate-diff.js [--out /path/to/audit.json]
+ */
+const { Pool } = require('pg');
+const fs = require('fs');
+const { validateBeforeActivate } = require('./lib/validate-before-activate.js');
+
+const pool = process.env.DATABASE_URL
+ ? new Pool({ connectionString: process.env.DATABASE_URL })
+ : new Pool({ host: '/tmp', database: 'dw_unified', user: process.env.PGUSER || process.env.USER });
+
+const oi = process.argv.indexOf('--out');
+const OUT = oi >= 0 ? process.argv[oi + 1] : `/tmp/anna-french-gate-diff-${new Date().toISOString().slice(0, 10)}.json`;
+
+const GARBLED_HANDLE = /designerwallcoverings-los-angeles\b/i;
+function expectedPrefix(productType) { return productType === 'Fabric' ? 'DWAF' : 'DWAT'; }
+
+function gateShape(row) {
+ let imgs = [];
+ try { imgs = JSON.parse(row.all_images || '[]'); } catch { imgs = []; }
+ const allImgs = imgs.length ? imgs : (row.image_url ? [row.image_url] : []);
+ return {
+ title: row.clean_title || '',
+ dwSku: row.dw_sku || `${row.dw_prefix || expectedPrefix(row.product_type)}-PENDING`,
+ descriptionHtml: row.ai_description || '',
+ specs: { width: row.width, length: row.length, repeat: row.repeat_v, material: row.material, unitOfMeasure: row.cost_unit_of_measure },
+ vendorSpecs: { width: row.width, length: row.length, repeat: row.repeat_v, material: row.material, unitOfMeasure: row.cost_unit_of_measure },
+ images: allImgs,
+ vendorImages: allImgs,
+ // staging rows don't carry a materialized sample variant; the cadence creates the
+ // {DW_SKU}-Sample at import. Treat sample as satisfied for the readiness check and
+ // call it out separately so the gate measures DATA-readiness, not import mechanics.
+ variants: [{ sku: `${row.dw_prefix || expectedPrefix(row.product_type)}-PENDING-Sample` }],
+ };
+}
+
+async function run() {
+ const t0 = Date.now();
+ const { rows: staged } = await pool.query(
+ `SELECT id, mfr_sku, dw_sku, dw_prefix, pattern_name, color_name, collection, product_type,
+ material, width, length, repeat_v, match_type, cost, cost_unit_of_measure,
+ ai_description, clean_title, clean_handle, image_url, all_images,
+ discontinued, image_rejected
+ FROM anna_french_catalog WHERE mfr_sku IS NOT NULL ORDER BY id`
+ );
+ const { rows: live } = await pool.query(
+ `SELECT upper(mfr_sku) AS mfr, dw_sku, mfr_sku, handle, product_type, status,
+ title, variant_sku
+ FROM shopify_products WHERE vendor='Anna French' AND mfr_sku IS NOT NULL`
+ );
+ const liveByMfr = new Map();
+ for (const l of live) { if (!liveByMfr.has(l.mfr)) liveByMfr.set(l.mfr, []); liveByMfr.get(l.mfr).push(l); }
+
+ const report = [];
+ const tally = {
+ staged: staged.length, live: live.length,
+ gate_pass: 0, gate_fail: 0,
+ discontinued_archive: 0, image_rejected: 0,
+ 'keep-as-is': 0, 're-prefix': 0, 'repair-handle': 0,
+ 'gate-pass-now': 0, 'still-fails-gate': 0, 'new-no-live-match': 0,
+ };
+ const reasonHist = {};
+
+ for (const row of staged) {
+ if (row.discontinued) { tally.discontinued_archive++; continue; }
+ if (row.image_rejected) tally.image_rejected++;
+
+ const gate = validateBeforeActivate(gateShape(row));
+ if (gate.ok) tally.gate_pass++; else { tally.gate_fail++; gate.reasons.forEach(r => reasonHist[r] = (reasonHist[r] || 0) + 1); }
+
+ const mfr = String(row.mfr_sku).toUpperCase();
+ const matches = liveByMfr.get(mfr) || [];
+ const wantPrefix = expectedPrefix(row.product_type);
+ const actions = new Set();
+
+ if (!matches.length) {
+ actions.add('new-no-live-match');
+ tally['new-no-live-match']++;
+ } else {
+ for (const m of matches) {
+ const liveSku = String(m.dw_sku || m.variant_sku || '');
+ const livePrefix = (liveSku.match(/^(DW[A-Z]{2})/) || [])[1] || null;
+ if (livePrefix && livePrefix !== wantPrefix) actions.add('re-prefix');
+ if (m.handle && (GARBLED_HANDLE.test(m.handle) || /^[a-z0-9]+designerwallcoverings/i.test(m.handle))) actions.add('repair-handle');
+ // type mismatch (live product_type vs deterministic type from mfr code)
+ if (m.product_type && m.product_type !== row.product_type) actions.add('re-prefix');
+ }
+ if (gate.ok) { actions.add('gate-pass-now'); tally['gate-pass-now']++; }
+ else { actions.add('still-fails-gate'); tally['still-fails-gate']++; }
+ if (actions.size === 1 && actions.has('gate-pass-now')) { actions.clear(); actions.add('keep-as-is'); tally['keep-as-is']++; }
+ else {
+ if (actions.has('re-prefix')) tally['re-prefix']++;
+ if (actions.has('repair-handle')) tally['repair-handle']++;
+ }
+ }
+
+ report.push({
+ mfr_sku: row.mfr_sku, product_type: row.product_type,
+ want_prefix: wantPrefix, clean_title: row.clean_title, clean_handle: row.clean_handle,
+ gate_ok: gate.ok, gate_reasons: gate.reasons, gate_tags: gate.tags,
+ live_matches: matches.length,
+ actions: [...actions],
+ });
+ }
+
+ const out = { generated: new Date().toISOString(), tally, gate_fail_reasons: reasonHist, products: report };
+ fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
+
+ console.log(`=== STEP 4: ONBOARDING GATE (${staged.length} staged, ${tally.discontinued_archive} discontinued→archive excluded) ===`);
+ console.log(`PASS: ${tally.gate_pass} FAIL: ${tally.gate_fail} (image_rejected: ${tally.image_rejected})`);
+ console.log('Fail reasons:', JSON.stringify(reasonHist, null, 0));
+ console.log(`\n=== STEP 5: DIFF vs LIVE ${tally.live} (join on mfr_sku) ===`);
+ console.log(` keep-as-is: ${tally['keep-as-is']}`);
+ console.log(` re-prefix: ${tally['re-prefix']}`);
+ console.log(` repair-handle: ${tally['repair-handle']}`);
+ console.log(` gate-pass-now: ${tally['gate-pass-now']}`);
+ console.log(` still-fails-gate: ${tally['still-fails-gate']}`);
+ console.log(` new-no-live-match: ${tally['new-no-live-match']}`);
+ console.log(` discontinued→archive: ${tally.discontinued_archive}`);
+ console.log(`\nAudit JSON: ${OUT} (${Math.round((Date.now() - t0) / 1000)}s)`);
+ await pool.end();
+}
+run().catch(e => { console.error('FATAL', e); process.exit(1); });
← 785b0977 Anna French STEP2+3: local Full-Monte enrichment + interior-
·
back to Designer Wallcoverings
·
auto-save: 2026-06-25T09:03:03 (3 files) — shopify/scripts/c 3866a1e5 →