[object Object]

← 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

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 →