[object Object]

← 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

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 →