[object Object]

← back to Designer Wallcoverings

Add Tres Tintas final completeness audit (desc/specs-by-type/room across all 592, roll-vs-mural spec shape aware)

f0d8c26f7535e1c8c2781e1377aaecadd7a1f783 · 2026-06-23 12:03:26 -0700 · Steve Abrams

Files touched

Diff

commit f0d8c26f7535e1c8c2781e1377aaecadd7a1f783
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 23 12:03:26 2026 -0700

    Add Tres Tintas final completeness audit (desc/specs-by-type/room across all 592, roll-vs-mural spec shape aware)
---
 shopify/scripts/tres-tintas-final-audit.js | 136 +++++++++++++++++++++++++++++
 1 file changed, 136 insertions(+)

diff --git a/shopify/scripts/tres-tintas-final-audit.js b/shopify/scripts/tres-tintas-final-audit.js
new file mode 100644
index 00000000..e8b0f989
--- /dev/null
+++ b/shopify/scripts/tres-tintas-final-audit.js
@@ -0,0 +1,136 @@
+#!/usr/bin/env node
+/**
+ * tres-tintas-final-audit.js
+ *
+ * Final completeness audit across ALL Tres Tintas products. For each product
+ * reports whether it has:
+ *   - DESC      : a real description (>120 chars), no old embedded spec table,
+ *                 no banned word "Wallpaper"
+ *   - SPECS     : correct shape for its type —
+ *                   ROLL  (non-mural): width + length + repeat + weight +
+ *                         match + finish + material + origin
+ *                   MURAL (/mural/ ref, M-prefix): material/weight/finish/origin
+ *                         (width/repeat legitimately absent — NOT a defect)
+ *   - ROOM      : a 2nd image OR an image with a room/setting alt
+ *
+ * Classifies each product COMPLETE / INCOMPLETE and lists the misses.
+ * Read-only. Writes a JSON report to tres-tintas-room-backups/_final-audit.json.
+ *
+ *   node tres-tintas-final-audit.js
+ */
+const fs = require('fs');
+const path = require('path');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-01';
+function envval(key) {
+  const env = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
+  const m = env.match(new RegExp(`^${key}=["']?([^"'\\n]+)`, 'm'));
+  return m ? m[1].trim() : null;
+}
+const TOKEN = envval('SHOPIFY_DRAFT_TOKEN');
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function fetchRetry(url, opts, tries = 3, timeoutMs = 45000) {
+  let e; for (let i = 0; i < tries; i++) { try { return await fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) }); } catch (err) { e = err; await sleep(800 * (i + 1)); } } throw e;
+}
+async function gql(query, variables) {
+  const r = await fetchRetry(`https://${STORE}/admin/api/${API}/graphql.json`, { method: 'POST', headers: H, body: JSON.stringify({ query, variables }) });
+  const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
+}
+
+// Is this a mural? mural refs are M-prefix (M2503-1) or have a /mural/ tag/ref.
+function isMural(title, tags) {
+  const refTag = (tags || []).find(t => /^pattern-ref-/i.test(t));
+  const ref = refTag ? refTag.replace(/^pattern-ref-/i, '') : '';
+  const titleSku = (/\b([A-Za-z]{1,3}\d{3,4}-\d+)\b/.exec(title || '') || [])[1] || '';
+  const sku = ref || titleSku;
+  return /^M\d/i.test(sku);   // M-prefix = mural quality
+}
+
+async function main() {
+  if (!TOKEN) throw new Error('missing SHOPIFY_DRAFT_TOKEN');
+  const q = `query($cursor:String){ products(first:50, query:"vendor:'Tres Tintas'", after:$cursor){
+    pageInfo{ hasNextPage endCursor }
+    edges{ node{ id title status tags descriptionHtml
+      images(first:10){ edges{ node{ altText } } }
+      metafields(first:50){ edges{ node{ namespace key value } } } } } } }`;
+
+  let cursor = null;
+  const rows = [];
+  while (true) {
+    const conn = (await gql(q, { cursor })).products;
+    for (const e of conn.edges) {
+      const n = e.node;
+      const id = n.id.split('/').pop();
+      const tags = (n.tags || []).map(t => t.trim());
+      const mf = {};
+      for (const m of n.metafields.edges) mf[`${m.node.namespace}.${m.node.key}`] = m.node.value;
+      const has = (k) => mf[k] && String(mf[k]).trim() !== '';
+
+      // DESC
+      const desc = (n.descriptionHtml || '');
+      const descOk = desc.length > 120 && !/<table/i.test(desc) && !/\bwallpaper\b/i.test(desc);
+
+      // ROOM (2nd image or room-alt)
+      const alts = n.images.edges.map(x => (x.node.altText || ''));
+      const roomOk = n.images.edges.length > 1 || alts.some(a => /room|setting|interior|ambiente/i.test(a));
+
+      // SPECS by type
+      const mural = isMural(n.title, tags);
+      let specsOk, specMiss = [];
+      const need = mural
+        ? ['custom.product_weight', 'global.Finish', 'custom.material', 'custom.origin']
+        : ['custom.width', 'custom.length', 'custom.pattern_repeat', 'custom.product_weight', 'global.MATCH', 'global.Finish', 'custom.material', 'custom.origin'];
+      for (const k of need) if (!has(k)) specMiss.push(k);
+      specsOk = specMiss.length === 0;
+
+      const complete = descOk && roomOk && specsOk;
+      rows.push({ id, title: n.title, status: n.status, type: mural ? 'mural' : 'roll',
+        descOk, roomOk, specsOk, specMiss, complete });
+    }
+    if (conn.pageInfo.hasNextPage) cursor = conn.pageInfo.endCursor; else break;
+  }
+
+  const active = rows.filter(r => r.status === 'ACTIVE');
+  const archived = rows.filter(r => r.status === 'ARCHIVED');
+  const summarize = (set) => ({
+    total: set.length,
+    complete: set.filter(r => r.complete).length,
+    descMiss: set.filter(r => !r.descOk).length,
+    specMiss: set.filter(r => !r.specsOk).length,
+    roomMiss: set.filter(r => !r.roomOk).length,
+  });
+
+  const report = {
+    generatedAt: new Date().toISOString(),
+    overall: { total: rows.length, complete: rows.filter(r => r.complete).length },
+    active: summarize(active),
+    archived: summarize(archived),
+    rolls: summarize(rows.filter(r => r.type === 'roll')),
+    murals: summarize(rows.filter(r => r.type === 'mural')),
+    incompleteActive: active.filter(r => !r.complete).map(r => ({
+      id: r.id, title: r.title, type: r.type,
+      miss: [!r.descOk && 'desc', !r.roomOk && 'room', !r.specsOk && `specs(${r.specMiss.join(',')})`].filter(Boolean),
+    })),
+  };
+
+  const out = path.join(__dirname, '..', 'tres-tintas-room-backups', '_final-audit.json');
+  fs.writeFileSync(out, JSON.stringify(report, null, 2));
+
+  console.log('=== TRES TINTAS FINAL AUDIT ===');
+  console.log(`overall: ${report.overall.complete}/${report.overall.total} complete`);
+  console.log(`ACTIVE  : ${active.length} total | complete ${report.active.complete} | desc-miss ${report.active.descMiss} | spec-miss ${report.active.specMiss} | room-miss ${report.active.roomMiss}`);
+  console.log(`ARCHIVED: ${archived.length} total | complete ${report.archived.complete} | (archived excluded from go-live gate)`);
+  console.log(`  rolls  : ${report.rolls.complete}/${report.rolls.total} complete`);
+  console.log(`  murals : ${report.murals.complete}/${report.murals.total} complete`);
+  if (report.incompleteActive.length) {
+    console.log(`\nINCOMPLETE ACTIVE (${report.incompleteActive.length}):`);
+    for (const r of report.incompleteActive) console.log(`  ${r.id} [${r.type}] ${r.title}  -> ${r.miss.join(' | ')}`);
+  } else {
+    console.log('\nAll ACTIVE products COMPLETE.');
+  }
+  console.log(`\nreport -> ${out}`);
+}
+main().catch(e => { console.error(e); process.exit(1); });

← ea390dcd Spec recrawl: title-SKU fallback for the 22 active Tres Tint  ·  back to Designer Wallcoverings  ·  dw-vendor-collection-url: use collection.id existence test + caa2d35e →