← back to Dw Yolo Loop

artmura-site/showroom.js

107 lines

/**
 * Showroom-only guard for the DW vendor-landing generator (TK-11200).
 *
 * Steve's standing rule (showroom-line-steward): a SHOWROOM-ONLY line — currently
 * Phillip Jeffries — must stay "addressable but not discoverable". It may be reached
 * by a direct /products/<handle> URL or on-site search, but must NOT appear on any
 * push/browse/discovery surface. A dedicated public vendor microsite (a full browsable
 * grid on its own subdomain) IS a discovery surface, so the generator must refuse to
 * mint one for a showroom-only vendor.
 *
 * Self-contained by design: this file is rsync'd with the app on a BUNDLE=1 deploy, so
 * the deployed microsite carries its own copy of the rule (no repo-relative ../ reads).
 * config/showroom-vendors.json is kept in sync with the canonical list at
 * ~/Projects/fix-live-board/config/showroom-vendors.json.
 *
 * Mirrors dw-domain-fleet/shared/catalog.js so both fleets speak the same rule.
 */
'use strict';
const fs = require('fs');
const path = require('path');

const SHOWROOM_VENDORS = (() => {
  try {
    const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'config', 'showroom-vendors.json'), 'utf8'));
    return (Array.isArray(raw) ? raw : []).map(v => norm(v)).filter(Boolean);
  } catch { return []; }
})();
// squashed forms, so a SLUG or a SUBDOMAIN label ("philipjeffries", "phillip-jeffries")
// matches the spaced vendor name ("Phillip Jeffries"). deploy-vendor.sh passes slugs.
const SHOWROOM_SQUASHED = SHOWROOM_VENDORS.map(v => v.replace(/ /g, ''));

// Normalize for comparison. Also collapses the well-known "Philip"/"Phillip" spelling
// split — the live catalog carries BOTH ("Phillip Jeffries" vendor, "philip-jeffries"
// handles), and a one-letter typo must not be a way past the guard.
function norm(v) {
  return String(v || '')
    .trim().toLowerCase()
    .replace(/[^a-z0-9]+/g, ' ')
    .replace(/philip/g, 'phillip')   // single-L typo -> canonical; 'phillip' can't re-match
    .replace(/\s+/g, ' ')
    .trim();
}
// same normalization with ALL separators removed — matches slugs/subdomain labels.
function squash(v) { return norm(v).replace(/ /g, ''); }

/** true if a vendor NAME (string) is a showroom-only line */
function isShowroomVendorName(name) {
  const n = norm(name), q = squash(name);
  if (!n) return false;
  return SHOWROOM_VENDORS.includes(n) || SHOWROOM_SQUASHED.includes(q);
}

/** true if a PRODUCT's vendor is a showroom-only line */
function isShowroomVendor(p) {
  return isShowroomVendorName(p && p.vendor);
}

/**
 * Product-level showroom TAG (TK-11307). A shared-vendor showroom line (e.g. MDC under
 * the shared "Phillipe Romano" label, which also carries SELLABLE lines) is hidden by its
 * "Showroom" tag WITHOUT hiding the vendor's sellable products. Exact tag, not a token.
 */
function isShowroomTagged(p) {
  const tags = p && p.tags;
  const arr = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(',') : []);
  return arr.some(t => String(t).trim().toLowerCase() === 'showroom');
}

/** showroom-only = vendor on the list OR carries the Showroom tag */
function isShowroomProduct(p) {
  return isShowroomVendor(p) || isShowroomTagged(p);
}

/**
 * The escape hatch. Building or serving a showroom line as a microsite is a deliberate,
 * Steve-gated act — never an accident. ALLOW_SHOWROOM=1 opts in and always logs loudly.
 */
function showroomOverride() {
  return process.env.ALLOW_SHOWROOM === '1';
}

/**
 * Hard gate used by build-line.js / server.js / deploy-vendor.sh.
 * Returns true when the caller may proceed; otherwise prints why and returns false.
 */
function assertNotShowroom(vendorName, what) {
  if (!isShowroomVendorName(vendorName)) return true;
  if (showroomOverride()) {
    console.warn(`[showroom] ⚠ ALLOW_SHOWROOM=1 — proceeding with ${what} for SHOWROOM-ONLY vendor "${vendorName}". This publishes a discovery surface for a showroom line.`);
    return true;
  }
  console.error(
    `\n[showroom] REFUSED: "${vendorName}" is a SHOWROOM-ONLY line.\n` +
    `  ${what} would create a public, browsable discovery surface for it, which breaks the\n` +
    `  "addressable but not discoverable" rule (showroom-line-steward).\n` +
    `  Showroom-only vendors: ${SHOWROOM_VENDORS.join(', ') || '(none configured)'}\n` +
    `  If this is deliberate and Steve-approved, re-run with ALLOW_SHOWROOM=1.\n`
  );
  return false;
}

module.exports = {
  SHOWROOM_VENDORS, SHOWROOM_SQUASHED, norm, squash,
  isShowroomVendorName, isShowroomVendor, isShowroomTagged, isShowroomProduct,
  showroomOverride, assertNotShowroom,
};