← back to Wallco Ai
scripts/audit-hard-rules.js
97 lines
#!/usr/bin/env node
/**
* audit-hard-rules.js — one command that verifies the data-integrity hard rules
* from CLAUDE.md still hold against the live catalog + shipped snapshot.
*
* These are the rules where a silent regression is customer-facing or a privacy
* leak, and where nothing else catches the drift after the fact:
*
* 1. Mural categories are NEVER tileable (delegates to audit-mural-kinds.js)
* — 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).
*
* Usage: node scripts/audit-hard-rules.js
*/
'use strict';
const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');
let failed = false;
const section = (n) => console.log(`\n### ${n}`);
// --- Rule 1: mural categories never tileable (reuse the canonical checker) ---
section('1. mural categories are never tileable');
{
const r = spawnSync('node', [path.join(__dirname, 'audit-mural-kinds.js')], { encoding: 'utf8' });
process.stdout.write(r.stdout || '');
if (r.stderr) process.stderr.write(r.stderr);
if (r.status !== 0) { failed = true; console.log(' -> FAIL'); }
else console.log(' -> PASS');
}
// --- 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]);
}
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'} ===`);
process.exit(failed ? 1 : 0);