[object Object]

← back to Wallco Ai

wallco generators: wire settlement gate into both launchd ticks

6ae57215cf8d09fcf5f5d199d260f582b5a7ad08 · 2026-05-19 08:05:56 -0700 · SteveStudio2

- new settlement_tick_guard.js wraps settlement-gate.js (pre-gen text
  check) + settlement_postgen_vision_check.js (Gemini image vision)
- generator_tick.js + drunk_animals_tick.js now: gate prompt BEFORE
  generation (BLOCK/NEEDS_REVIEW abort the tick), vision-verify the
  produced image AFTER, quarantine (unpublish + stamp settlement_verdict)
  on prohibited; vision errors fail CLOSED
- generator_tick only fans colour variations off a cleanly-OK parent
- fix: snapshot max(id) before generation so an empty/failed generation
  can never stamp or quarantine a pre-existing design
- spoon_all_designs gains settlement_verdict column

Files touched

Diff

commit 6ae57215cf8d09fcf5f5d199d260f582b5a7ad08
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 19 08:05:56 2026 -0700

    wallco generators: wire settlement gate into both launchd ticks
    
    - new settlement_tick_guard.js wraps settlement-gate.js (pre-gen text
      check) + settlement_postgen_vision_check.js (Gemini image vision)
    - generator_tick.js + drunk_animals_tick.js now: gate prompt BEFORE
      generation (BLOCK/NEEDS_REVIEW abort the tick), vision-verify the
      produced image AFTER, quarantine (unpublish + stamp settlement_verdict)
      on prohibited; vision errors fail CLOSED
    - generator_tick only fans colour variations off a cleanly-OK parent
    - fix: snapshot max(id) before generation so an empty/failed generation
      can never stamp or quarantine a pre-existing design
    - spoon_all_designs gains settlement_verdict column
---
 scripts/drunk_animals_tick.js    | 53 +++++++++++++++++++++++++
 scripts/generator_tick.js        | 85 ++++++++++++++++++++++++++++++----------
 scripts/settlement_tick_guard.js | 80 +++++++++++++++++++++++++++++++++++++
 3 files changed, 198 insertions(+), 20 deletions(-)

diff --git a/scripts/drunk_animals_tick.js b/scripts/drunk_animals_tick.js
index 88890ad..e578424 100644
--- a/scripts/drunk_animals_tick.js
+++ b/scripts/drunk_animals_tick.js
@@ -11,6 +11,7 @@
 const { spawnSync, execSync } = require('child_process');
 const path = require('path');
 const { buildPrompt, ANIMALS, STYLES } = require('./drunk_animal_prompts');
+const { pregenGate, postgenVision, quarantine, stampVerdict } = require('./settlement_tick_guard');
 
 // Cross-tick dedup — read the last N drunk-animal prompts from PG and
 // extract the animal phrase + style phrase from each. Reject any candidate
@@ -63,6 +64,24 @@ const ROOT = path.join(__dirname, '..');
 const prompt = freshPrompt();
 console.log(`[tick ${new Date().toISOString()}] ${prompt.slice(0, 100)}…`);
 
+// ── Settlement pre-generation gate ───────────────────────────────────────
+// No pattern is generated until the gate returns OK. BLOCK / NEEDS_REVIEW
+// abort the tick before any image is produced.
+const gate = pregenGate(prompt);
+if (!gate.ok) {
+  console.log(`[tick] SETTLEMENT ${gate.verdict} — skipping generation. ${gate.reason}`);
+  process.exit(0);
+}
+
+// Snapshot the max id BEFORE generation so a failed/empty generation can
+// never make the post-gen check stamp a pre-existing design.
+let maxIdBefore = 0;
+try {
+  maxIdBefore = parseInt(execSync(
+    `psql dw_unified -At -c "SELECT COALESCE(max(id),0) FROM spoon_all_designs;"`,
+    { encoding: 'utf8' }).trim(), 10) || 0;
+} catch {}
+
 const r = spawnSync('node', [
   path.join(ROOT, 'scripts', 'generate_designs.js'),
   '--n', '1',
@@ -71,6 +90,40 @@ const r = spawnSync('node', [
   '--prompts', prompt,
 ], { stdio: 'inherit', cwd: ROOT, timeout: 4 * 60_000 });
 
+// ── Settlement post-generation vision check ──────────────────────────────
+// The produced image must be visually verified — text anti-prompts are not
+// enforceable on the image model.
+if (r.status === 0) {
+  let designId = null;
+  try {
+    const raw = execSync(
+      `psql dw_unified -At -c "SELECT id FROM spoon_all_designs ` +
+      `WHERE category='drunk-animals' AND id > ${maxIdBefore} ORDER BY id DESC LIMIT 1;"`,
+      { encoding: 'utf8' }).trim();
+    designId = raw ? parseInt(raw, 10) : null;
+  } catch {}
+  if (!designId) {
+    console.log('[tick] no new design produced (generation backend returned nothing) — nothing to verify');
+  } else {
+    let local = null;
+    try {
+      local = execSync(
+        `psql dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=${designId};"`,
+        { encoding: 'utf8' }).trim();
+    } catch {}
+    const vision = postgenVision(local, prompt);
+    if (vision.prohibited || vision.error) {
+      const v = vision.error ? 'REVIEW-VISION-ERROR' : 'BLOCK';
+      quarantine(designId, v, vision.error
+        ? 'post-gen vision unavailable — fail-closed' : vision.raw);
+      console.log(`[tick] SETTLEMENT QUARANTINE design ${designId} → ${v}`);
+    } else {
+      stampVerdict(designId, vision.verdict === 'OK' ? 'OK' : 'NEEDS_REVIEW');
+      console.log(`[tick] settlement ${vision.verdict} — design ${designId}`);
+    }
+  }
+}
+
 // Refresh designs.json snapshot from PG, then ping reload endpoint so the
 // live server picks up the new row in its in-memory grid.
 spawnSync('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
diff --git a/scripts/generator_tick.js b/scripts/generator_tick.js
index 5b37c30..76a389e 100644
--- a/scripts/generator_tick.js
+++ b/scripts/generator_tick.js
@@ -16,6 +16,7 @@
 require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
 const path = require('path');
 const { execSync, spawnSync } = require('child_process');
+const { pregenGate, postgenVision, quarantine, stampVerdict } = require('./settlement_tick_guard');
 
 const DB = 'dw_unified';
 const ROOT = path.join(__dirname, '..');
@@ -112,6 +113,19 @@ function main() {
   }
   const seeds = recentPaletteSeeds(3);
   const prompt = buildPrompt(recipe, seeds);
+
+  // ── Settlement pre-generation gate ─────────────────────────────────────
+  // Hard rule: no pattern is generated until the gate returns OK. BLOCK and
+  // NEEDS_REVIEW both abort the tick before any image is produced.
+  const gate = pregenGate(prompt);
+  if (!gate.ok) {
+    console.log(JSON.stringify({
+      event: 'tick_skipped_settlement', recipe_id: recipe.id,
+      recipe_name: recipe.name, verdict: gate.verdict, reason: gate.reason, prompt,
+    }));
+    return 0;
+  }
+
   const seedIds = seeds.map(s => s.id);
   const runId = recordRunStart(recipe.id, prompt, seedIds);
   console.log(JSON.stringify({
@@ -122,6 +136,11 @@ function main() {
   // Spawn generator
   const backend = process.env.GEN_BACKEND || 'replicate';
   const env = { ...process.env, GEN_BACKEND: backend };
+  // Snapshot max id BEFORE generation — generate_designs.js can exit 0 while
+  // producing nothing (e.g. backend out of credit), so identity-by-newness
+  // is the only safe way to find the row this tick actually created.
+  let maxIdBefore = 0;
+  try { maxIdBefore = parseInt(psql('SELECT COALESCE(max(id),0) FROM spoon_all_designs;'), 10) || 0; } catch {}
   const r = spawnSync('node', [
     path.join(ROOT, 'scripts', 'generate_designs.js'),
     '--n', '1', '--kind', 'seamless_tile',
@@ -136,21 +155,43 @@ function main() {
     return 1;
   }
 
-  // Find the design ID that just got inserted
+  // Find the design this tick inserted — must be NEWER than the snapshot,
+  // else generation produced nothing and there is nothing to verify.
   let designId = null;
   try {
-    designId = parseInt(psql(`
-      SELECT id FROM spoon_all_designs ORDER BY id DESC LIMIT 1;
-    `), 10);
+    const raw = psql(`SELECT id FROM spoon_all_designs WHERE id > ${maxIdBefore} ORDER BY id DESC LIMIT 1;`);
+    designId = raw ? parseInt(raw, 10) : null;
   } catch {}
 
+  if (!designId) {
+    finishRun(runId, false, null, 'generation produced no design row');
+    console.log(JSON.stringify({ event: 'tick_no_design', run_id: runId }));
+    return 1;
+  }
+
   finishRun(runId, true, designId, null);
   bumpRecipe(recipe.id);
 
-  // ── Auto-rate the freshly-generated design ─────────────────────────────
-  // Two-lens AI critique (graphic-designer + interior-designer) via Gemini Vision.
-  // Detached so the cron doesn't block waiting for ~5-10s of vision calls.
+  // ── Settlement post-generation vision check ────────────────────────────
+  // Text anti-prompts are not enforceable on the image model — the produced
+  // image must be visually verified before it can be rated or bred from.
   if (designId) {
+    let local = null;
+    try { local = psql(`SELECT local_path FROM spoon_all_designs WHERE id=${designId};`); } catch {}
+    const vision = postgenVision(local, recipe.name || prompt);
+    if (vision.prohibited || vision.error) {
+      const v = vision.error ? 'REVIEW-VISION-ERROR' : 'BLOCK';
+      quarantine(designId, v, vision.error
+        ? 'post-gen vision unavailable — fail-closed' : vision.raw);
+      console.log(JSON.stringify({
+        event: 'settlement_quarantine', design_id: designId, verdict: v,
+      }));
+      return 0;   // never rate or breed variations off a prohibited/unverified design
+    }
+    const clean = vision.verdict === 'OK';
+    stampVerdict(designId, clean ? 'OK' : 'NEEDS_REVIEW');
+
+    // Auto-rate — harmless two-lens AI critique, runs for OK and NEEDS REVIEW.
     try {
       const { spawn } = require('child_process');
       const rater = spawn('node',
@@ -161,19 +202,23 @@ function main() {
       console.log(JSON.stringify({ event: 'rate_spawn_err', err: e.message }));
     }
 
-    // ── Auto-fan-out 5 color variations of the new design ────────────────
-    // Up to 5 hue/sat/val shifts (hue +60, +120, +180, -60, mono). Each gets
-    // watermarked + inserted into spoon_all_designs + queued for AI rating.
-    try {
-      const { spawn } = require('child_process');
-      const varN = parseInt(process.env.GENERATOR_VARIATIONS || '5', 10);
-      const vars = spawn('node',
-        [path.join(__dirname, 'auto_variations.js'),
-         '--parent', String(designId), '--n', String(varN)],
-        { detached: true, stdio: 'ignore', env: process.env });
-      vars.unref();
-    } catch (e) {
-      console.log(JSON.stringify({ event: 'var_spawn_err', err: e.message }));
+    // Auto-fan-out colour variations — ONLY off a cleanly-OK design. A
+    // NEEDS_REVIEW parent is not bred until a human clears it. (Hue/sat/val
+    // shifts cannot introduce Part A/B motifs, so OK children stay OK.)
+    if (clean) {
+      try {
+        const { spawn } = require('child_process');
+        const varN = parseInt(process.env.GENERATOR_VARIATIONS || '5', 10);
+        const vars = spawn('node',
+          [path.join(__dirname, 'auto_variations.js'),
+           '--parent', String(designId), '--n', String(varN)],
+          { detached: true, stdio: 'ignore', env: process.env });
+        vars.unref();
+      } catch (e) {
+        console.log(JSON.stringify({ event: 'var_spawn_err', err: e.message }));
+      }
+    } else {
+      console.log(JSON.stringify({ event: 'settlement_needs_review', design_id: designId }));
     }
   }
 
diff --git a/scripts/settlement_tick_guard.js b/scripts/settlement_tick_guard.js
new file mode 100644
index 0000000..c19813c
--- /dev/null
+++ b/scripts/settlement_tick_guard.js
@@ -0,0 +1,80 @@
+'use strict';
+/**
+ * Settlement guard for the launchd generator ticks.
+ *
+ * Wraps the two existing settlement modules so generator_tick.js and
+ * drunk_animals_tick.js can gate every generation without duplicating logic:
+ *
+ *   - pregenGate(prompt)        — fast, free text check (settlement-gate.js).
+ *                                 ok=false  => DO NOT call the image generator.
+ *   - postgenVision(path,title) — Gemini vision check on the produced PNG
+ *                                 (settlement_postgen_vision_check.js).
+ *   - quarantine(id,verdict,why)— unpublish + stamp the design row.
+ *   - stampVerdict(id,verdict)  — record an OK verdict on the design row.
+ *
+ * Hard rule (~/.claude/skills/settlement/SKILL.md): no wallpaper pattern is
+ * generated or published until the gate returns OK. Both BLOCK and
+ * NEEDS_REVIEW halt generation. A vision check that ERRORs fails CLOSED —
+ * the design is unpublished and flagged, never silently passed.
+ */
+const path = require('path');
+const { spawnSync, execSync } = require('child_process');
+const { checkPrompt, format } = require('./settlement-gate');
+
+const ROOT = path.join(__dirname, '..');
+const PSQL = (process.platform === 'linux')
+  ? 'sudo -n -u postgres psql dw_unified -At -q'
+  : 'psql dw_unified -At -q';
+
+function psql(sql) {
+  return execSync(PSQL, { input: sql, encoding: 'utf8' }).trim();
+}
+
+// --- pre-generation: text gate. ok=false means do not generate. ----------
+function pregenGate(prompt) {
+  const r = checkPrompt(prompt);
+  return {
+    ok: r.verdict === 'OK',
+    verdict: r.verdict,                 // OK | NEEDS_REVIEW | BLOCK
+    reason: r.reason || '',
+    detail: format(prompt, r),
+  };
+}
+
+// --- post-generation: Gemini vision on the produced image ----------------
+// Returns { verdict, prohibited, error }. verdict is BLOCK|NEEDS REVIEW|OK|ERROR.
+function postgenVision(localPath, title) {
+  const r = spawnSync('node', [
+    path.join(__dirname, 'settlement_postgen_vision_check.js'),
+    localPath, title || '',
+  ], { cwd: ROOT, encoding: 'utf8', timeout: 90_000, env: process.env });
+  const out = `${r.stdout || ''}${r.stderr || ''}`;
+  const m = out.match(/VERDICT:\s*(BLOCK|NEEDS REVIEW|OK)/);
+  if (r.status === 2 || (m && m[1] === 'BLOCK')) {
+    return { verdict: 'BLOCK', prohibited: true, error: false, raw: out.slice(-500) };
+  }
+  if (m) return { verdict: m[1], prohibited: false, error: false, raw: out.slice(-500) };
+  // No parseable verdict — vision unavailable. FAIL CLOSED.
+  return { verdict: 'ERROR', prohibited: false, error: true, raw: out.slice(-500) };
+}
+
+// --- unpublish + stamp a design that failed the gate ---------------------
+function quarantine(designId, verdict, reason) {
+  if (!designId) return;
+  const note = `SETTLEMENT ${verdict}: ${String(reason || '').replace(/'/g, "''").slice(0, 380)}`;
+  psql(`
+    UPDATE spoon_all_designs
+       SET is_published = FALSE,
+           settlement_verdict = '${verdict.replace(/'/g, "''")}',
+           notes = COALESCE(notes, '') || ' | ${note}'
+     WHERE id = ${designId};
+  `);
+}
+
+// --- record a clean verdict ----------------------------------------------
+function stampVerdict(designId, verdict) {
+  if (!designId) return;
+  psql(`UPDATE spoon_all_designs SET settlement_verdict='${verdict.replace(/'/g, "''")}' WHERE id=${designId};`);
+}
+
+module.exports = { pregenGate, postgenVision, quarantine, stampVerdict };

← 3127fc6 feat(mobile-chrome): meta theme-color cream/dark per prefers  ·  back to Wallco Ai  ·  Add social share layer + reel maker to wallco.ai 383c03b →