← back to Wallco Ai
Add publish-ok-murals.sh: OK-only mural auto-publish + verified redeploy (DTD-C back-half)
09482ea807df76b4a32f565f1df94b56ec213872 · 2026-06-03 21:41:52 -0700 · Steve
Files touched
A scripts/publish-ok-murals.sh
Diff
commit 09482ea807df76b4a32f565f1df94b56ec213872
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 3 21:41:52 2026 -0700
Add publish-ok-murals.sh: OK-only mural auto-publish + verified redeploy (DTD-C back-half)
---
scripts/publish-ok-murals.sh | 150 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 150 insertions(+)
diff --git a/scripts/publish-ok-murals.sh b/scripts/publish-ok-murals.sh
new file mode 100755
index 0000000..aca0f70
--- /dev/null
+++ b/scripts/publish-ok-murals.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+# publish-ok-murals.sh — the "publish murals as they're created" back-half.
+#
+# DTD verdict 2026-06-03 (Option C, unanimous 2/2): generation stays MANUAL;
+# this script automates ONLY the deterministic, compliance-safe publish + deploy.
+#
+# What it does, every run (idempotent):
+# 1. PULL — unpublish any LIVE mural whose settlement verdict is non-OK
+# (BLOCK / NEEDS_REVIEW / REVIEW-VISION-ERROR). Compliance-safe,
+# needs no approval (removing blocked-live content is always allowed).
+# 2. PUBLISH — set is_published=true for every mural with settlement_verdict='OK'
+# that isn't live yet. HARD-FILTERED to 'OK' only — never NULL/ungated
+# (this is the fix for the old auto-curator NULL-leak that put ~205
+# un-gated murals live).
+# 3. DEPLOY — only if step 1 or 2 changed anything: rebuild data/designs.json
+# (refresh_designs_snapshot.py has its own >50% shrink guard), assert
+# 0 live BLOCK as a final gate, then ./deploy-kamatera.sh.
+#
+# Safe to run on a timer. Uses Mac2 peer-auth psql (no secrets in this file).
+#
+# Usage:
+# scripts/publish-ok-murals.sh # full run: pull + publish + deploy-if-changed
+# scripts/publish-ok-murals.sh --no-deploy # DB-only (pull + publish), skip deploy
+# scripts/publish-ok-murals.sh --dry-run # report counts, change nothing
+# scripts/publish-ok-murals.sh --force-deploy # deploy even if nothing changed
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+PROJ="$(pwd)"
+LOG_TS() { date '+%Y-%m-%d %H:%M:%S'; }
+say() { echo "[$(LOG_TS)] $*"; }
+
+NO_DEPLOY=0; DRY_RUN=0; FORCE_DEPLOY=0
+for a in "$@"; do
+ case "$a" in
+ --no-deploy) NO_DEPLOY=1 ;;
+ --dry-run) DRY_RUN=1 ;;
+ --force-deploy) FORCE_DEPLOY=1 ;;
+ *) echo "unknown arg: $a" >&2; exit 2 ;;
+ esac
+done
+
+# ---- single-runner lock (macOS-native; mkdir is atomic, no flock dependency) ----
+LOCKDIR="$PROJ/.publish-ok-murals.lock.d"
+if ! mkdir "$LOCKDIR" 2>/dev/null; then
+ # stale-lock takeover: if the holder PID is dead, reclaim it
+ if [ -f "$LOCKDIR/pid" ] && ! kill -0 "$(cat "$LOCKDIR/pid" 2>/dev/null)" 2>/dev/null; then
+ say "stale lock (holder dead) — reclaiming"
+ rm -rf "$LOCKDIR"; mkdir "$LOCKDIR" 2>/dev/null || { say "lost lock race — exiting"; exit 0; }
+ else
+ say "another publish-ok-murals run holds the lock — exiting"
+ exit 0
+ fi
+fi
+echo $$ > "$LOCKDIR/pid"
+trap 'rm -rf "$LOCKDIR"' EXIT
+
+DB="dw_unified"
+psql() { command psql -d "$DB" -v ON_ERROR_STOP=1 "$@"; }
+
+# Canonical mural predicate (kept in sync with lib/mural-categories.js + scenic).
+MURAL_PRED="(
+ category IN ('monterey-mural','cactus-pine-scenic','cactus-11ft-mural','cactus-accent-mural','tree-mural','mural-scenic')
+ OR category LIKE 'designer-scenic%'
+ OR kind IN ('mural','mural_panel')
+)"
+
+# ---- snapshot of current state ----
+read_state() {
+ psql -tA -F'|' -c "
+ SELECT
+ count(*) FILTER (WHERE is_published AND settlement_verdict='OK' AND $MURAL_PRED) AS live_ok_murals,
+ count(*) FILTER (WHERE NOT is_published AND settlement_verdict='OK' AND COALESCE(user_removed,false)=false AND local_path IS NOT NULL AND $MURAL_PRED) AS publishable,
+ count(*) FILTER (WHERE is_published AND settlement_verdict IN ('BLOCK','NEEDS_REVIEW','REVIEW-VISION-ERROR') AND $MURAL_PRED) AS live_nonok,
+ count(*) FILTER (WHERE is_published AND settlement_verdict='BLOCK') AS live_block_global
+ FROM all_designs;"
+}
+
+IFS='|' read -r LIVE_OK PUBLISHABLE LIVE_NONOK LIVE_BLOCK_GLOBAL <<<"$(read_state)"
+say "state: live_ok_murals=$LIVE_OK publishable_now=$PUBLISHABLE live_nonok_murals=$LIVE_NONOK live_block_global=$LIVE_BLOCK_GLOBAL"
+
+if [ "$DRY_RUN" = 1 ]; then
+ say "--dry-run: would PULL $LIVE_NONOK non-OK live murals and PUBLISH $PUBLISHABLE OK murals. No changes made."
+ exit 0
+fi
+
+CHANGED=0
+
+# ---- 1. PULL non-OK live murals (compliance-safe) ----
+if [ "$LIVE_NONOK" -gt 0 ]; then
+ PULLED=$(psql -tA -c "
+ WITH upd AS (
+ UPDATE all_designs SET is_published=false,
+ notes = COALESCE(notes,'') || ' | pulled (publish-ok-murals): settlement ' || settlement_verdict || ' ' || to_char(now(),'YYYY-MM-DD')
+ WHERE is_published AND settlement_verdict IN ('BLOCK','NEEDS_REVIEW','REVIEW-VISION-ERROR') AND $MURAL_PRED
+ RETURNING 1) SELECT count(*) FROM upd;")
+ say "PULLED $PULLED non-OK live murals"
+ [ "$PULLED" -gt 0 ] && CHANGED=1
+fi
+
+# ---- 2. PUBLISH OK murals (hard-filtered to settlement_verdict='OK') ----
+if [ "$PUBLISHABLE" -gt 0 ]; then
+ PUBBED=$(psql -tA -c "
+ BEGIN;
+ SET LOCAL settlement.allow_override='true';
+ WITH upd AS (
+ UPDATE all_designs SET is_published=true, user_removed=false,
+ notes = COALESCE(notes,'') || ' | published (publish-ok-murals): settlement OK ' || to_char(now(),'YYYY-MM-DD')
+ WHERE settlement_verdict='OK' AND NOT is_published
+ AND COALESCE(user_removed,false)=false AND local_path IS NOT NULL AND $MURAL_PRED
+ RETURNING 1) SELECT count(*) FROM upd;
+ COMMIT;" | tr -d '[:space:]')
+ say "PUBLISHED $PUBBED OK murals"
+ [ "${PUBBED:-0}" -gt 0 ] && CHANGED=1
+fi
+
+if [ "$NO_DEPLOY" = 1 ]; then
+ say "--no-deploy: DB updated, skipping snapshot+deploy"
+ exit 0
+fi
+
+if [ "$CHANGED" = 0 ] && [ "$FORCE_DEPLOY" = 0 ]; then
+ say "nothing changed — skipping deploy (use --force-deploy to override)"
+ exit 0
+fi
+
+# ---- 3. VERIFIED DEPLOY ----
+# Final compliance gate: assert 0 live BLOCK globally before we build the snapshot.
+LIVE_BLOCK=$(psql -tA -c "SELECT count(*) FROM all_designs WHERE is_published AND settlement_verdict='BLOCK';" | tr -d '[:space:]')
+if [ "${LIVE_BLOCK:-0}" -ne 0 ]; then
+ say "ABORT DEPLOY: $LIVE_BLOCK live BLOCK rows remain (pull them first). Snapshot NOT built."
+ exit 1
+fi
+
+say "rebuilding data/designs.json (shrink-guard enforced by refresh_designs_snapshot.py)…"
+python3 scripts/refresh_designs_snapshot.py 2>&1 | grep -E "Wrote|REFUSE|guard|shrink|ERROR" || true
+if ! grep -q '"id"' data/designs.json 2>/dev/null && [ ! -s data/designs.json ]; then
+ say "ABORT DEPLOY: designs.json looks empty after rebuild"; exit 1
+fi
+
+say "deploying to prod via deploy-kamatera.sh…"
+./deploy-kamatera.sh 2>&1 | tail -6
+
+# ---- post-deploy: log win to CNCP (best-effort) ----
+TOTAL_LIVE=$(psql -tA -c "SELECT count(*) FROM all_designs WHERE is_published;" | tr -d '[:space:]')
+curl -s -X POST http://127.0.0.1:3333/api/wins -H 'Content-Type: application/json' \
+ -d "{\"project\":\"wallco-ai\",\"title\":\"Auto-published gate-clean murals + redeployed\",\"summary\":\"publish-ok-murals.sh: pulled non-OK live murals, published settlement-OK murals (hard-filtered), redeployed. Live total ${TOTAL_LIVE}.\"}" \
+ >/dev/null 2>&1 || true
+
+say "done. live total=$TOTAL_LIVE"
← 0edfa2a pdp: inject room-size-mural block on the LIVE-default-theme
·
back to Wallco Ai
·
Add launchd plist for standing OK-only mural auto-publish (3 b01a644 →