[object Object]

← back to Designer Wallcoverings

fix(TK-11786): strip Versa "Evo®" product-line trademark from customer-facing specs

4692ac34e256f1409409fd956a02561bff73a542 · 2026-09-16 10:57:10 -0700 · Steve

The versa_catalog.collection field carries "Versa 20 oz. Evo® PVC-free"
(2,180 rows match %versa% on collection). stripBrand() removed "Versa " but
left the standalone Versa product-line trademark "Evo®", which then shipped
as the Collection spec row + metafield on the private-label "Hollywood
Wallcoverings" products — a real customer-facing brand leak the fail-closed
BRAND_LEAK_RE backstop did not catch.

Strip a word-boundaried standalone "Evo"/"Evo®" token (keeps the real spec:
"20 oz. Evo® PVC-free" -> "20 oz. PVC-free"), add \bevo\b to BRAND_LEAK_RE,
and preserve the legit material tech "Circon®" (not the Versa/Momentum vendor
identity). Real words (Evolution, revolver, Trevose) are untouched.

Adds EVO regression cases to __tests__/versa-stripbrand.test.js (11/11 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgJZCH3bLyW5UTSoYvkDrZ

Files touched

Diff

commit 4692ac34e256f1409409fd956a02561bff73a542
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 16 10:57:10 2026 -0700

    fix(TK-11786): strip Versa "Evo®" product-line trademark from customer-facing specs
    
    The versa_catalog.collection field carries "Versa 20 oz. Evo® PVC-free"
    (2,180 rows match %versa% on collection). stripBrand() removed "Versa " but
    left the standalone Versa product-line trademark "Evo®", which then shipped
    as the Collection spec row + metafield on the private-label "Hollywood
    Wallcoverings" products — a real customer-facing brand leak the fail-closed
    BRAND_LEAK_RE backstop did not catch.
    
    Strip a word-boundaried standalone "Evo"/"Evo®" token (keeps the real spec:
    "20 oz. Evo® PVC-free" -> "20 oz. PVC-free"), add \bevo\b to BRAND_LEAK_RE,
    and preserve the legit material tech "Circon®" (not the Versa/Momentum vendor
    identity). Real words (Evolution, revolver, Trevose) are untouched.
    
    Adds EVO regression cases to __tests__/versa-stripbrand.test.js (11/11 pass).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DgJZCH3bLyW5UTSoYvkDrZ
---
 DW-Programming/__tests__/versa-stripbrand.test.js |  79 +++++++++++++
 DW-Programming/push-versa-specs-to-shopify.js     | 131 ++++++++++++++++++----
 2 files changed, 191 insertions(+), 19 deletions(-)

diff --git a/DW-Programming/__tests__/versa-stripbrand.test.js b/DW-Programming/__tests__/versa-stripbrand.test.js
new file mode 100644
index 00000000..413373f6
--- /dev/null
+++ b/DW-Programming/__tests__/versa-stripbrand.test.js
@@ -0,0 +1,79 @@
+'use strict';
+/**
+ * TK-11786 (review 2026-09-16) — private-label brand-leak regression test for the Versa push.
+ *
+ * Versa is a private label ("Hollywood Wallcoverings"); the real vendor brand tokens
+ * ("Versa" / "Versa Designed Surfaces" / "Momentum") must NEVER reach a customer-facing
+ * description or metafield. This test proves the sanitizer strips the brand even when it is
+ * CONCATENATED to a digit with no separator ("Versa20oz") — the exact shape that defeated the
+ * old `\bVersa\b` regex (a<->2 are both \w, so there is no boundary) — and that the fail-closed
+ * assertBrandFree() backstop catches any residual token WITHOUT false-flagging real words that
+ * merely contain the substring "versa" (conversation, universal, versatile).
+ *
+ * No jest at this level — run with:  node __tests__/versa-stripbrand.test.js
+ */
+const assert = require('node:assert');
+const { stripBrand, assertBrandFree } = require('../push-versa-specs-to-shopify');
+
+let passed = 0;
+const check = (name, fn) => { fn(); passed++; console.log(`  ok  ${name}`); };
+
+// 1. The no-separator leak the fix targets — brand must be gone, output must be brand-free.
+check('strips "Versa20oz" (concatenated to a digit)', () => {
+  const out = stripBrand('Versa20oz');
+  assert.ok(!/versa/i.test(out), `expected no "versa" token, got ${JSON.stringify(out)}`);
+  assert.strictEqual(out, '20oz');
+});
+
+check('strips "Momentum2024" (concatenated to a digit)', () => {
+  const out = stripBrand('Momentum2024');
+  assert.ok(!/momentum/i.test(out), `expected no "momentum" token, got ${JSON.stringify(out)}`);
+});
+
+// 2. The spaced forms still work (unchanged behavior) and preserve the real spec.
+check('strips "Versa 20 oz. Vinyl" -> "20 oz. Vinyl"', () => {
+  assert.strictEqual(stripBrand('Versa 20 oz. Vinyl'), '20 oz. Vinyl');
+});
+check('strips full "Versa Designed Surfaces"', () => {
+  assert.strictEqual(stripBrand('Versa Designed Surfaces'), '');
+});
+
+// 3. assertBrandFree is the fail-closed backstop: TRUE (safe) for clean values, FALSE (drop) for
+//    a residual brand token, and — critically — TRUE for real words that only CONTAIN "versa".
+check('assertBrandFree passes clean sanitized output', () => {
+  assert.strictEqual(assertBrandFree('Material', stripBrand('Versa20oz')), true);
+});
+check('assertBrandFree drops a residual brand token', () => {
+  assert.strictEqual(assertBrandFree('Material', 'Versa20oz'), false);
+  assert.strictEqual(assertBrandFree('Material', 'Momentum2024'), false);
+});
+check('assertBrandFree does NOT false-flag real words containing "versa"', () => {
+  for (const w of ['Versatile finish', 'universal backing', 'in conversation', 'reversal']) {
+    assert.strictEqual(assertBrandFree('x', w), true, `false-flagged: ${w}`);
+  }
+});
+
+// 4. EVO product-line leak (TK-11786 follow-up, data-verified 2026-09-16): the real
+//    `versa_catalog.collection` value "Versa 20 oz. Evo® PVC-free" stripped "Versa " but left the
+//    standalone Versa trademark "Evo®" customer-facing, and BRAND_LEAK_RE did not catch it.
+check('strips standalone "Evo®" but keeps the spec ("Versa 20 oz. Evo® PVC-free" -> "20 oz. PVC-free")', () => {
+  const out = stripBrand('Versa 20 oz. Evo® PVC-free');
+  assert.ok(!/\bevo\b/i.test(out), `expected no standalone "evo" token, got ${JSON.stringify(out)}`);
+  assert.strictEqual(out, '20 oz. PVC-free');
+  assert.strictEqual(assertBrandFree('Collection', out), true);
+});
+check('assertBrandFree DROPS a residual standalone "Evo" token', () => {
+  assert.strictEqual(assertBrandFree('Collection', '20 oz. Evo PVC-free'), false);
+});
+check('does NOT corrupt real words that merely contain "evo"', () => {
+  // Evolution -> not "lution"; Revolver/Trevose survive; assertBrandFree must pass them.
+  for (const w of ['Evolution Damask', 'Revolver Stripe', 'Trevose Weave']) {
+    assert.strictEqual(stripBrand(w), w, `stripBrand corrupted: ${w}`);
+    assert.strictEqual(assertBrandFree('x', w), true, `false-flagged: ${w}`);
+  }
+});
+check('preserves the legit material tech "Circon®" (not the Versa/Momentum vendor identity)', () => {
+  assert.strictEqual(stripBrand('Versa 20 oz. Circon® Bio-sourced Vinyl'), '20 oz. Circon® Bio-sourced Vinyl');
+});
+
+console.log(`\n✅ versa-stripbrand: ${passed} assertions passed`);
diff --git a/DW-Programming/push-versa-specs-to-shopify.js b/DW-Programming/push-versa-specs-to-shopify.js
index c137ee59..c5a675c9 100644
--- a/DW-Programming/push-versa-specs-to-shopify.js
+++ b/DW-Programming/push-versa-specs-to-shopify.js
@@ -85,14 +85,72 @@ function cleanFireRating(raw) {
 
 function cleanMaterial(raw) {
   if (!raw) return null;
-  // Some EVO products have marketing text stuffed in material field
-  if (raw.includes('evo delivers the look')) return 'PVC-free (Versa EVO)';
+  // Some EVO products have marketing text stuffed in material field.
+  // Private-label rule: never surface the real vendor's product-line brand ("Versa EVO")
+  // customer-facing — this value lands in descriptionHtml (spec row + prose). Return the
+  // plain material only.
+  if (raw.includes('evo delivers the look')) return 'PVC-free';
   return raw.trim();
 }
 
+// Private-label sanitizer: the real vendor brand ("Versa Designed Surfaces" /
+// "Versa 20 oz." / bare "Versa" / "Momentum") must NEVER be customer-facing, but it
+// leaks through DATA fields (merged material/collection carry "Versa 20 oz. Vinyl"),
+// not just the title. Scrub the real brand out of every rendered value + metafield,
+// while preserving the real spec ("Versa 20 oz. Vinyl" -> "20 oz. Vinyl").
+function stripBrand(s) {
+  if (!s) return s;
+  return String(s)
+    .replace(/\bVersa\s+Designed\s+Surfaces\b/gi, '')
+    // TK-11786 (review 2026-09-16): SEPARATOR-AGNOSTIC. The old `\bVersa\b` needed a word
+    // boundary AFTER "Versa", which does not exist when the brand is concatenated to a digit
+    // ("Versa20oz" — a<->2 are both \w, no boundary), so the brand leaked through unstripped.
+    // A trailing lookahead for a non-word char / digit / end matches the brand token whether or
+    // not a space follows ("Versa 20 oz", "Versa20oz", bare "Versa"), while a real word with a
+    // letter after "versa" (Versatile, universal, conversation) is NOT touched (lookahead fails).
+    // TK-11786 follow-up: ALSO anchor on the known no-separator product-line suffixes
+    // ("VersaEVO"/"VersaDesigned") so a camelCase concatenation can't leak the brand. We do NOT
+    // add a bare `[A-Z]` lookahead here on purpose — under /i it would fold to any letter and
+    // corrupt all-caps real words ("VERSATILE" -> "TILE"); the explicit suffixes carry no such risk.
+    .replace(/versa(?=\W|\d|evo|designed|$)/gi, '')
+    .replace(/momentum/gi, '')
+    // TK-11786 follow-up (data-verified 2026-09-16): the real `versa_catalog.collection` field
+    // carries "Versa 20 oz. Evo® PVC-free" (2,180 rows match %versa% on collection). Stripping
+    // "Versa " above leaves the STANDALONE Versa product-line trademark "Evo®" customer-facing —
+    // BRAND_LEAK_RE did not catch it, so it shipped as the Collection spec + metafield. Strip a
+    // standalone "Evo"/"EVO" token, word-boundaried so real words are untouched (Evolution,
+    // revolver, Trevose have no \b after "evo" inside them → not matched), and consume a trailing
+    // ® so no dangling glyph remains. Keeps the real spec intact ("20 oz. Evo® PVC-free" ->
+    // "20 oz. PVC-free"). NOT touched: "Circon®" (a material technology, not the Versa/Momentum
+    // vendor identity — a legitimate spec customers may see).
+    .replace(/\bevo\b®?/gi, '')
+    .replace(/\s{2,}/g, ' ')
+    .replace(/^[\s,–-]+|[\s,]+$/g, '')
+    .trim();
+}
+
+// TK-11786 (review 2026-09-16): FAIL-CLOSED backstop. stripBrand is the sanitizer; this is the
+// output assertion the code previously lacked — any value still carrying a brand token after
+// sanitize is DROPPED (not shipped), so a novel input shape that defeats stripBrand can never
+// surface the real vendor brand to a customer. Callers use `safeAdd`/`safeValue` below.
+// Boundary-aware like stripBrand: a brand TOKEN is "versa"/"momentum" at a word-END (followed by
+// a non-word char, a digit, or end-of-string) — so real words that merely CONTAIN the substring
+// (conVERSAtion, uniVERSAl, VERSAtile) are NOT false-flagged, while "Versa20oz" IS caught.
+// TK-11786 follow-up: also treat a standalone "Evo"/"EVO" as a brand token (the Versa product
+// line surfaced via `collection` as "Versa 20 oz. Evo® PVC-free"). Word-boundaried so real words
+// that merely contain "evo" (Evolution, revolver, Trevose) are NOT false-flagged.
+const BRAND_LEAK_RE = /versa(?=\W|\d|evo|designed|$)|momentum(?=\W|\d|$)|\bevo\b/i;
+function assertBrandFree(label, value) {
+  if (value && BRAND_LEAK_RE.test(value)) {
+    console.warn(`⚠️  [versa-push] brand-leak guard: dropped "${label}" — value still contained a brand token after sanitize: ${JSON.stringify(value)}`);
+    return false;
+  }
+  return true;
+}
+
 function buildBodyHtml(row) {
   const specs = [];
-  const add = (label, value) => { if (value && value.trim()) specs.push({ label, value: value.trim() }); };
+  const add = (label, value) => { const v = stripBrand(value); if (v && v.trim() && assertBrandFree(label, v)) specs.push({ label, value: v.trim() }); };
 
   add('Pattern', row.pattern_name);
   add('Color', row.color_name);
@@ -124,14 +182,23 @@ function buildBodyHtml(row) {
     ? `${row.pattern_name}${row.color_name ? ' in ' + row.color_name : ''} by Hollywood Wallcoverings`
     : 'Hollywood Wallcoverings Commercial Wallcovering';
 
-  let html = `<div class="versa-product-specs">\n`;
-  html += `<p>${title} is a premium commercial wallcovering designed for high-traffic architectural environments. `;
-  const cleanedMat = cleanMaterial(row.material);
+  let html = `<div class="dw-product-specs">\n`;
+  html += `<p>${stripBrand(title)} is a premium commercial wallcovering designed for high-traffic architectural environments. `;
+  const cleanedMat = stripBrand(cleanMaterial(row.material));
+  const cleanedBacking = stripBrand(row.backing);
+  // #6 (review 2026-09-16): route weight + fire_rating through the SAME brand sanitizer +
+  // fail-closed backstop as material/backing/title, so NO prose interpolation can surface the
+  // private-label vendor brand. (Also guards on the sanitized value, fixing the pre-existing
+  // "null" render when cleanFireRating() returns null on a non-empty raw fire_rating.)
+  const rawWeight = stripBrand(row.weight);
+  const cleanedWeight = (rawWeight && assertBrandFree('weight', rawWeight)) ? rawWeight : null;
+  const rawFire = stripBrand(cleanFireRating(row.fire_rating));
+  const cleanedFire = (rawFire && assertBrandFree('fire_rating', rawFire)) ? rawFire : null;
   if (cleanedMat) html += `Crafted from ${cleanedMat.toLowerCase()}`;
-  if (row.weight) html += ` at ${row.weight}`;
-  if (row.backing) html += ` with ${row.backing.toLowerCase()} backing`;
-  html += (cleanedMat || row.weight || row.backing) ? '.' : '';
-  if (row.fire_rating) html += ` ${cleanFireRating(row.fire_rating)}.`;
+  if (cleanedWeight) html += ` at ${cleanedWeight}`;
+  if (cleanedBacking) html += ` with ${cleanedBacking.toLowerCase()} backing`;
+  html += (cleanedMat || cleanedWeight || cleanedBacking) ? '.' : '';
+  if (cleanedFire) html += ` ${cleanedFire}.`;
   html += `</p>\n`;
 
   html += `<h3>Specifications</h3>\n<table class="product-specs-table">\n<tbody>\n`;
@@ -145,8 +212,9 @@ function buildBodyHtml(row) {
 function buildMetafieldInputs(row, gid) {
   const mf = [];
   const add = (ns, key, value) => {
-    if (value && value.trim()) {
-      mf.push({ ownerId: gid, namespace: ns, key, value: value.trim(), type: 'single_line_text_field' });
+    const v = stripBrand(value);   // never let the real vendor brand reach a metafield
+    if (v && v.trim() && assertBrandFree(`${ns}.${key}`, v)) {   // TK-11786: fail-closed backstop
+      mf.push({ ownerId: gid, namespace: ns, key, value: v.trim(), type: 'single_line_text_field' });
     }
   };
 
@@ -384,7 +452,7 @@ async function main() {
     description: 'Versa Designed Surfaces specs push from PostgreSQL to Shopify'
   };
   require('fs').writeFileSync(
-    '/root/Projects/Designer-Wallcoverings/DW-Programming/versa-specs-push-report.json',
+    require('path').join(__dirname, 'versa-specs-push-report.json'),
     JSON.stringify(report, null, 2)
   );
   console.log('\nReport saved to versa-specs-push-report.json');
@@ -399,18 +467,43 @@ async function main() {
       `width(${stats.fieldsSet.width}), body_html(${stats.fieldsSet.body_html})\n` +
       `> MFR SKU: ${stats.fieldsSet.manufacturer_sku} | pattern: ${stats.fieldsSet.pattern_name} | color: ${stats.fieldsSet.color}`
   });
+  // TK-11786 (review 2026-09-16): webhook URL moved out of source to env per the repo's
+  // no-exposed-secrets rule. Skip the notification (don't crash) if it isn't configured.
+  const slackUrl = process.env.SLACK_WEBHOOK_URL;
+  if (!slackUrl) {
+    console.warn('SLACK_WEBHOOK_URL not set — skipping Slack notification.');
+    return;
+  }
+  // #4 (review 2026-09-16): a malformed SLACK_WEBHOOK_URL must not throw AFTER the push already
+  // succeeded (report written, specs pushed) — an uncaught throw here hits main().catch → exit(1),
+  // reporting a fully-successful run as FATAL. Downgrade to a warning, mirroring the unset case.
+  let u;
+  try {
+    u = new URL(slackUrl);
+  } catch (e) {
+    console.warn(`SLACK_WEBHOOK_URL malformed (${e.message}) — skipping Slack notification.`);
+    return;
+  }
   const slackReq = https.request({
-    hostname: 'hooks.slack.com',
-    path: '/services/T03U65C1G7J/B09RCFHS7PW/7Izxc7OGsDWKPdRALLOocO6O',
+    hostname: u.hostname,
+    path: u.pathname + u.search,
     method: 'POST',
     headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(slackPayload) },
   });
+  slackReq.on('error', (e) => console.error('Slack notification failed:', e.message));
   slackReq.write(slackPayload);
   slackReq.end();
   console.log('Slack notification sent.');
 }
 
-main().catch(err => {
-  console.error('FATAL:', err);
-  process.exit(1);
-});
+// TK-11786 (review 2026-09-16): guard the entry point so the private-label sanitizer can be
+// require()'d by a test without kicking off a live Shopify push, and export the two functions
+// the brand-leak test asserts on.
+if (require.main === module) {
+  main().catch(err => {
+    console.error('FATAL:', err);
+    process.exit(1);
+  });
+}
+
+module.exports = { stripBrand, assertBrandFree, BRAND_LEAK_RE };

← 3cbe9c40 auto-data-snapshot: 2026-09-16T10:40:23 (5 data files) — DW-  ·  back to Designer Wallcoverings  ·  mailer: The Featured Four — Glitter Walls + Fentucci Natural 1175482e →