[object Object]

← back to Wallco Ai

server: passesAestheticGate runtime filter in loadDesigns (neon + ≥4 hues + rainbow tags)

c1c8a8973a5e0d9e305480548d2a0fec0098849c · 2026-05-19 23:35:44 -0700 · Steve

Files touched

Diff

commit c1c8a8973a5e0d9e305480548d2a0fec0098849c
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 19 23:35:44 2026 -0700

    server: passesAestheticGate runtime filter in loadDesigns (neon + ≥4 hues + rainbow tags)
---
 server.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 81 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index dde93f7..f022e67 100644
--- a/server.js
+++ b/server.js
@@ -237,11 +237,90 @@ function loadBlankIds() {
   }
 }
 
+// ── Aesthetic gate (Steve 2026-05-19) ───────────────────────────────────────
+// Defence-in-depth against neon + multi-hue patterns. The DB-level purge
+// (scripts/purge-neon-and-multicolor.js) already removed the offenders, but
+// this runtime filter protects against any reseed / refresh_designs_snapshot
+// run that might re-introduce a row matching the rule. Drops:
+//   - design where any palette entry is neon (HSL S≥70, L 45-75, pct≥5)
+//   - design where palette has ≥ 4 distinct hue buckets
+//   - design where dominant_hex alone is neon (single-color fallback for
+//     rows without a full palette)
+//   - design where prompt / motifs / tags match the neon/rainbow regex
+function _hexToHsl(hex) {
+  if (!hex || typeof hex !== 'string') return null;
+  const m = hex.trim().match(/^#?([0-9a-f]{6})$/i);
+  if (!m) return null;
+  const n = parseInt(m[1], 16);
+  const r = ((n >> 16) & 0xff) / 255;
+  const g = ((n >> 8)  & 0xff) / 255;
+  const b = ( n        & 0xff) / 255;
+  const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
+  const l  = (mx + mn) / 2;
+  let h = 0, s = 0;
+  if (mx !== mn) {
+    const d = mx - mn;
+    s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
+    switch (mx) {
+      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+      case g: h = (b - r) / d + 2; break;
+      case b: h = (r - g) / d + 4; break;
+    }
+    h *= 60;
+  }
+  return { h, s: s * 100, l: l * 100 };
+}
+function _hueBucket(hex) {
+  const hsl = _hexToHsl(hex);
+  if (!hsl) return 'unknown';
+  if (hsl.s < 12 || hsl.l < 10 || hsl.l > 90) return 'neutral';
+  const h = hsl.h;
+  if (h < 15) return 'red'; if (h < 45) return 'orange'; if (h < 65) return 'yellow';
+  if (h < 90) return 'chartreuse'; if (h < 150) return 'green'; if (h < 180) return 'teal';
+  if (h < 210) return 'cyan'; if (h < 250) return 'blue'; if (h < 280) return 'indigo';
+  if (h < 310) return 'violet'; if (h < 340) return 'magenta'; return 'red';
+}
+function _isNeonHex(hex, pct) {
+  const hsl = _hexToHsl(hex);
+  if (!hsl) return false;
+  return hsl.s >= 70 && hsl.l >= 45 && hsl.l <= 75 && (pct == null || pct >= 5);
+}
+const _AESTHETIC_GATE_RE = /\b(neon|fluorescent|electric|rainbow|day[- ]?glo|highlighter|fluoro)\b/i;
+function passesAestheticGate(d) {
+  if (!d) return false;
+  // 1) palette pass (if present)
+  const palette = Array.isArray(d.palette) ? d.palette : null;
+  if (palette) {
+    const buckets = new Set();
+    for (const p of palette) {
+      const b = _hueBucket(p.hex);
+      if (b !== 'neutral' && b !== 'unknown') buckets.add(b);
+      if (_isNeonHex(p.hex, p.pct)) return false;
+    }
+    if (buckets.size >= 4) return false;
+  } else if (d.dominant_hex) {
+    // 2) single-color fallback when no palette is available
+    if (_isNeonHex(d.dominant_hex, 100)) return false;
+  }
+  // 3) tag / prompt / motifs regex pass
+  const hay = [
+    d.prompt || '',
+    Array.isArray(d.tags)   ? d.tags.join(' ')   : '',
+    Array.isArray(d.motifs) ? d.motifs.join(' ') : '',
+  ].join(' ');
+  if (_AESTHETIC_GATE_RE.test(hay)) return false;
+  return true;
+}
+
 function loadDesigns() {
   try {
     const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
-    // Hide soft-removed designs from all listings/sorts.
-    DESIGNS = raw.filter(d => !d.user_removed);
+    // Hide soft-removed designs, then drop anything that fails the aesthetic
+    // gate (neon / >3 hues / rainbow tags).
+    const beforeGate = raw.filter(d => !d.user_removed);
+    DESIGNS = beforeGate.filter(passesAestheticGate);
+    const dropped = beforeGate.length - DESIGNS.length;
+    if (dropped > 0) console.log(`[aesthetic-gate] dropped ${dropped} designs at load (neon/multi-hue/rainbow)`);
     let max = 0;
     for (const d of DESIGNS) {
       const t = Date.parse(d.created_at || '');

← dec2cf7 generator hardening: tone-on-tone suffix + neon/rainbow nega  ·  back to Wallco Ai  ·  reconcile generator suffix → empirical professional palette 9abdaaa →