[object Object]

← back to Wallco Ai

audit: extend hard-rule gate to vendor-names + FLIEPAPER (4 rules, one snapshot load)

222ab358e4dcb05f9b0d56a0ce59ebcd664794f0 · 2026-06-04 09:18:00 -0700 · Steve Abrams

Rule 3 (no vendor names in public UI — the legal premise of wallco.ai) and
rule 4 (no FLIEPAPER word in any customer-facing surface) were CLAUDE.md hard
rules with no standing programmatic guard. Folded into the umbrella (no new
file): scans the 9 customer-visible snapshot fields x 26,060 rows against the
server.js VENDOR_DENYLIST + a fliepaper regex, loading designs.json once for
rules 2-4. Current state: ALL CLEAR on all four.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 222ab358e4dcb05f9b0d56a0ce59ebcd664794f0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 4 09:18:00 2026 -0700

    audit: extend hard-rule gate to vendor-names + FLIEPAPER (4 rules, one snapshot load)
    
    Rule 3 (no vendor names in public UI — the legal premise of wallco.ai) and
    rule 4 (no FLIEPAPER word in any customer-facing surface) were CLAUDE.md hard
    rules with no standing programmatic guard. Folded into the umbrella (no new
    file): scans the 9 customer-visible snapshot fields x 26,060 rows against the
    server.js VENDOR_DENYLIST + a fliepaper regex, loading designs.json once for
    rules 2-4. Current state: ALL CLEAR on all four.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/audit-hard-rules.js | 76 +++++++++++++++++++++++++++++++--------------
 1 file changed, 53 insertions(+), 23 deletions(-)

diff --git a/scripts/audit-hard-rules.js b/scripts/audit-hard-rules.js
index e7f1237..5e39161 100644
--- a/scripts/audit-hard-rules.js
+++ b/scripts/audit-hard-rules.js
@@ -10,6 +10,9 @@
  *      — a tile kind in a mural category renders as a broken tiny repeating tile.
  *   2. data/designs.json MUST NOT leak local_path  (or any /Users/<home> path)
  *      — that column leaks absolute home paths through the PUBLIC /api/designs.
+ *   3. No vendor names in public UI                (the legal premise of wallco.ai)
+ *      — a denylisted vendor name in any customer-visible field is an exposure.
+ *   4. No "FLIEPAPER" word in any customer-facing surface.
  *
  * Read-only. Never writes the DB or any file. Exit 0 = all clear, 1 = a rule is
  * violated (so it is usable as a pre-deploy gate / cron canary).
@@ -34,32 +37,59 @@ section('1. mural categories are never tileable');
   else console.log('  -> PASS');
 }
 
-// --- Rule 2: no local_path / home-path leak in the public snapshot ---
-section('2. data/designs.json carries no local_path / home-path leak');
-{
-  const jsonPath = path.join(__dirname, '..', 'data', 'designs.json');
-  if (!fs.existsSync(jsonPath)) {
-    console.log('  data/designs.json absent — skipped (nothing to leak)');
-  } else {
-    const parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
-    const rows = Array.isArray(parsed) ? parsed : (parsed.designs || parsed.items || []);
-    const withKey = [];
-    const withHome = [];
-    for (const row of rows) {
-      if (!row || typeof row !== 'object') continue;
-      if ('local_path' in row) withKey.push(row.id);
-      for (const [k, v] of Object.entries(row)) {
-        if (typeof v === 'string' && v.includes('/Users/stevestudio2')) { withHome.push([row.id, k]); break; }
-      }
+// --- Rules 2-4: scan the public snapshot once, run all field-level checks ---
+// Customer-visible string fields that reach the public PDP / grid / /api/designs.
+const PUBLIC_FIELDS = ['title', 'category', 'handle', 'ai_title', 'name', 'ai_pattern', 'ai_color_name', 'colorway', 'pattern_name'];
+// Vendor denylist — KEEP IN SYNC with server.js (the VENDOR_DENYLIST regex, ~line 875).
+const VENDOR_DENYLIST = /\b(thibaut|koroseal|schumacher|maya[ -]romanoff|scalamandre|arte[ -]international|cole[ -]?(and|&)[ -]?son|phillip[ -]jeffries|osborne[ -]?(and|&)?[ -]?little|de[ -]gournay|fromental|sister[ -]parish|dedar|brewster|mind[ -]the[ -]gap|hygge|designtex|wolf[ -]gordon|carnegie|coordonn|daisy[ -]bennett|china[ -]seas|york|marburg|romo|fabricut|stout|knoll|arteriors|timorous[ -]beasties|hangups)\b/i;
+const FLIEPAPER = /fliepaper/i;
+
+const jsonPath = path.join(__dirname, '..', 'data', 'designs.json');
+if (!fs.existsSync(jsonPath)) {
+  section('2-4. public snapshot checks');
+  console.log('  data/designs.json absent — skipped (nothing to leak)');
+} else {
+  const parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
+  const rows = Array.isArray(parsed) ? parsed : (parsed.designs || parsed.items || []);
+  const withKey = [];   // rule 2: local_path key
+  const withHome = [];  // rule 2: /Users home-path leak
+  const vendorHits = [];// rule 3
+  const flieHits = [];  // rule 4
+  for (const row of rows) {
+    if (!row || typeof row !== 'object') continue;
+    if ('local_path' in row) withKey.push(row.id);
+    for (const [k, v] of Object.entries(row)) {
+      if (typeof v !== 'string') continue;
+      if (v.includes('/Users/stevestudio2')) withHome.push([row.id, k]);
     }
-    console.log(`  rows=${rows.length}  local_path-key=${withKey.length}  home-path-leak=${withHome.length}`);
-    if (withKey.length || withHome.length) {
-      failed = true;
-      console.log(`  -> FAIL  local_path ids: ${withKey.slice(0, 10).join(', ')}  home-leak: ${JSON.stringify(withHome.slice(0, 10))}`);
-    } else {
-      console.log('  -> PASS');
+    for (const f of PUBLIC_FIELDS) {
+      const v = row[f];
+      if (typeof v !== 'string') continue;
+      const m = VENDOR_DENYLIST.exec(v);
+      if (m) vendorHits.push([row.id, f, m[0], v.slice(0, 50)]);
+      if (FLIEPAPER.test(v)) flieHits.push([row.id, f, v.slice(0, 50)]);
     }
   }
+
+  section('2. data/designs.json carries no local_path / home-path leak');
+  console.log(`  rows=${rows.length}  local_path-key=${withKey.length}  home-path-leak=${withHome.length}`);
+  if (withKey.length || withHome.length) {
+    failed = true;
+    console.log(`  -> FAIL  local_path ids: ${withKey.slice(0, 10).join(', ')}  home-leak: ${JSON.stringify(withHome.slice(0, 10))}`);
+  } else console.log('  -> PASS');
+
+  section('3. no vendor names in public-facing snapshot fields');
+  console.log(`  scanned ${PUBLIC_FIELDS.length} public fields x ${rows.length} rows`);
+  if (vendorHits.length) {
+    failed = true;
+    console.log(`  -> FAIL  ${vendorHits.length} hit(s): ${JSON.stringify(vendorHits.slice(0, 10))}`);
+  } else console.log('  -> PASS');
+
+  section('4. no FLIEPAPER word in public-facing snapshot fields');
+  if (flieHits.length) {
+    failed = true;
+    console.log(`  -> FAIL  ${flieHits.length} hit(s): ${JSON.stringify(flieHits.slice(0, 10))}`);
+  } else console.log('  -> PASS');
 }
 
 console.log(`\n=== HARD-RULE AUDIT: ${failed ? 'FAIL — a rule is violated (see above)' : 'ALL CLEAR'} ===`);

← d249fc8 audit: umbrella hard-rule gate (mural-kinds + designs.json l  ·  back to Wallco Ai  ·  test: make npm test a real preflight gate (node -c server.js 83390fc →