← 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
M scripts/drunk_animals_tick.jsM scripts/generator_tick.jsA scripts/settlement_tick_guard.js
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 →