← 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
A DW-Programming/__tests__/versa-stripbrand.test.jsM DW-Programming/push-versa-specs-to-shopify.js
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 →