← back to Wallco Ai
audit: standing read-only check that mural categories never carry a tile kind
24c17c0a74694c52e964f33e0a109b3b2a33a391 · 2026-06-04 09:13:52 -0700 · Steve Abrams
Insert-time assertMuralKind() only guards rows that go through it; the
2026-05-27 incident was 12 rows that bypassed it via direct SQL. This
catches that drift after the fact — requires lib/mural-categories.js +
lib/db.js (zero-drift canonical lists), exits 1 on LIVE violations so it
is CI/cron-able. Current state: CLEAN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/audit-mural-kinds.js
Diff
commit 24c17c0a74694c52e964f33e0a109b3b2a33a391
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 4 09:13:52 2026 -0700
audit: standing read-only check that mural categories never carry a tile kind
Insert-time assertMuralKind() only guards rows that go through it; the
2026-05-27 incident was 12 rows that bypassed it via direct SQL. This
catches that drift after the fact — requires lib/mural-categories.js +
lib/db.js (zero-drift canonical lists), exits 1 on LIVE violations so it
is CI/cron-able. Current state: CLEAN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/audit-mural-kinds.js | 86 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 86 insertions(+)
diff --git a/scripts/audit-mural-kinds.js b/scripts/audit-mural-kinds.js
new file mode 100644
index 0000000..6097166
--- /dev/null
+++ b/scripts/audit-mural-kinds.js
@@ -0,0 +1,86 @@
+#!/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();
← 8537412 damask line batch-3: +3 heritage damasks live (noir-rose, sl
·
back to Wallco Ai
·
audit: umbrella hard-rule gate (mural-kinds + designs.json l d249fc8 →