← back to Wallco Ai
scripts/audit-mural-kinds.js
87 lines
#!/usr/bin/env node
/**
* audit-mural-kinds.js — standing audit for the hard rule
* "Mural categories are NEVER tileable" (CLAUDE.md).
*
* The insert-time guard `assertMuralKind()` in lib/mural-categories.js only
* protects rows that go THROUGH it. The 2026-05-27 incident was 12 rows that
* BYPASSED it via direct SQL (inserted as kind='seamless_tile' into
* monterey-mural / cactus-pine-scenic) — each rendered as a tiny repeating tile
* on the /designs grid + PDP and looked broken. Nothing catches that class of
* drift after the fact. This script does: a read-only sweep that any session or
* cron can run to confirm the rule still holds.
*
* A mural-category row with a TILE kind (seamless_tile / tile / repeat_tile) is
* a VIOLATION. Published violations are customer-facing-broken (exit 1);
* unpublished ones are latent (warned, exit 0 unless --strict).
*
* Canonical category + tile-kind lists come straight from lib/mural-categories.js
* (single source of truth — no drift). DB read goes through lib/db.js psqlQuery
* (local psql, same config as server.js).
*
* Usage:
* node scripts/audit-mural-kinds.js # report; exit 1 on LIVE violations
* node scripts/audit-mural-kinds.js --strict # exit 1 on ANY violation (incl. unpublished)
* node scripts/audit-mural-kinds.js --fix-sql # print (do NOT run) the corrective UPDATE
*/
'use strict';
const { MURAL_CATEGORIES, TILE_KINDS, muralKindFor } = require('../lib/mural-categories.js');
const { psqlQuery } = require('../lib/db.js');
const STRICT = process.argv.includes('--strict');
const FIX_SQL = process.argv.includes('--fix-sql');
// Build an inline SQL list from the canonical sets so the query can never drift
// from lib/mural-categories.js. Values are static identifiers (no user input),
// but escape single-quotes defensively anyway.
const sqlList = (set) => [...set].map((s) => `'${String(s).replace(/'/g, "''")}'`).join(', ');
const MURAL_SQL = sqlList(MURAL_CATEGORIES);
const TILE_SQL = sqlList(TILE_KINDS);
function rows(sql) {
const out = psqlQuery(sql);
if (!out) return [];
return out.split('\n').filter(Boolean).map((line) => line.split('|'));
}
function main() {
// Violations: a mural-category row carrying a tile kind.
const bad = rows(
`SELECT id, lower(trim(category)), kind, is_published
FROM all_designs
WHERE lower(trim(category)) IN (${MURAL_SQL})
AND kind IN (${TILE_SQL})
ORDER BY is_published DESC, id`
);
const live = bad.filter((r) => r[3] === 't');
const latent = bad.filter((r) => r[3] !== 't');
console.log('=== mural-kind audit (hard rule: mural categories are never tileable) ===');
console.log(`categories checked: ${[...MURAL_CATEGORIES].join(', ')}`);
console.log(`tile kinds refused: ${[...TILE_KINDS].join(', ')}`);
console.log('');
if (!bad.length) {
console.log('RESULT: CLEAN — every mural-category row is mural_panel/mural. Rule holds.');
} else {
console.log(`RESULT: ${bad.length} VIOLATION(S) — ${live.length} LIVE (customer-facing-broken), ${latent.length} unpublished (latent):`);
for (const r of bad) {
console.log(` #${r[0]} ${r[1]} kind=${r[2]} ${r[3] === 't' ? 'LIVE' : 'unpublished'}`);
}
if (FIX_SQL) {
console.log('\n-- corrective SQL (review, then run by hand — this script never writes):');
for (const r of bad) {
console.log(`UPDATE all_designs SET kind='${muralKindFor(r[1])}' WHERE id=${r[0]};`);
}
} else {
console.log('\nrun with --fix-sql to print the corrective UPDATEs (script never writes the DB itself).');
}
}
const failed = live.length > 0 || (STRICT && bad.length > 0);
process.exit(failed ? 1 : 0);
}
main();