[object Object]

← back to Wallco Ai

fliepaper-bugs: API + server.js mount — settlement-gated regen, yolo job queue, GET /api/collections endpoint

b5d6362d72498cdc147c76776fe7a4c947078296 · 2026-05-13 16:54:29 -0700 · Steve

Files touched

Diff

commit b5d6362d72498cdc147c76776fe7a4c947078296
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 13 16:54:29 2026 -0700

    fliepaper-bugs: API + server.js mount — settlement-gated regen, yolo job queue, GET /api/collections endpoint
---
 server.js             |   7 ++
 src/fliepaper-bugs.js | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 262 insertions(+)

diff --git a/server.js b/server.js
index 3d76da7..10228f0 100644
--- a/server.js
+++ b/server.js
@@ -781,6 +781,13 @@ setInterval(() => load(true), 30000);
 // check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
 const { isAdmin, requireAdmin } = require('./src/admin-gate');
 
+// ── Fliepaper Bugs — Gucci Resort Edition (6 patterns × 6 colorways = 36 cells)
+// Settlement-gated regen routes a job file into data/fliepaper-bugs/queue/ that
+// yolo-runner picks up. Public collection page + admin grid; placeholders show
+// while yolo-runner generates art.
+try { require('./src/fliepaper-bugs').mount(app, { isAdmin }); }
+catch (e) { console.error('Fliepaper-bugs mount failed:', e.message); }
+
 // ── /admin/reload-designs — re-read data/designs.json without a pm2 reload.
 // Localhost-admin per src/admin-gate. Used by drunk-animals tick + future
 // generators to surface freshly persisted designs in the live grid without
diff --git a/src/fliepaper-bugs.js b/src/fliepaper-bugs.js
new file mode 100644
index 0000000..436b86f
--- /dev/null
+++ b/src/fliepaper-bugs.js
@@ -0,0 +1,255 @@
+// fliepaper-bugs collection API
+// Mounts:
+//   GET  /api/collections/fliepaper-bugs            — public, returns spec.json + per-cell status
+//   POST /api/collections/fliepaper-bugs/cell/regenerate — admin, settlement-gated, queues yolo regen
+//   GET  /collections/fliepaper-bugs                — public HTML
+//   GET  /admin/fliepaper-bugs                      — admin HTML (admin-gated)
+//
+// Data model:
+//   - spec.json:          /data/fliepaper-bugs/spec.json   (6 patterns × 6 colorways)
+//   - PG fliepaper_bugs_cells: status, design_id (FK spoon_all_designs.id), thumbnail_url, last_error
+//   - PG mp_collections row exists (slug='fliepaper-bugs', designer Wallco House)
+//
+// Settlement skill is invoked BEFORE every regenerate. The fliepaper-bugs prompts
+// are insect-only (no foliage/banana/butterfly) so they should always pass —
+// but the gate runs as a hard rule, never bypassed.
+
+'use strict';
+const fs   = require('fs');
+const path = require('path');
+const { spawn, execSync } = require('child_process');
+
+const SPEC_PATH = path.join(__dirname, '..', 'data', 'fliepaper-bugs', 'spec.json');
+
+// ── PG access — reuse the parent project's PSQL_CMD convention.
+const DB_NAME = process.env.WALLCO_DB || 'dw_unified';
+const PSQL_CMD = (process.platform === 'linux')
+  ? `sudo -n -u postgres psql ${DB_NAME} -At -q`
+  : `psql ${DB_NAME} -At -q`;
+
+function psqlQuery(sql) {
+  return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+}
+function pgEsc(v) {
+  if (v === null || v === undefined) return 'NULL';
+  if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'NULL';
+  if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
+  return "'" + String(v).replace(/'/g, "''") + "'";
+}
+
+// ── Spec loader (cached; spec.json is hand-edited)
+let SPEC_CACHE = null;
+let SPEC_MTIME = 0;
+function loadSpec() {
+  try {
+    const stat = fs.statSync(SPEC_PATH);
+    if (!SPEC_CACHE || stat.mtimeMs > SPEC_MTIME) {
+      SPEC_CACHE = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
+      SPEC_MTIME = stat.mtimeMs;
+    }
+    return SPEC_CACHE;
+  } catch (e) {
+    return null;
+  }
+}
+
+// ── Settlement gate (inline implementation of the binding rules from
+//    ~/.claude/skills/settlement/SKILL.md). For fliepaper-bugs the only
+//    risk surface is if a regen request adds foliage to the prompt; the
+//    insect motifs themselves are not prohibited. We block prompts that
+//    name any of the Part B elements (bananas/grapes/birds/butterflies)
+//    or any leaf/frond term. Returns { verdict, reason, suggestion? }.
+function settlementCheck(prompt) {
+  let p = String(prompt || '').toLowerCase();
+
+  // Strip negative-clause tails — they often LIST prohibited elements as
+  // anti-prompts ("no birds, no butterflies, no bananas") and would
+  // false-positive on a substring scan. We check only the affirmative prompt.
+  // Anything from a "no <word>", "without", "excluding", "no foliage" cue to
+  // end-of-sentence is removed before the substring scan runs.
+  p = p
+    .replace(/\bno foliage[^.]*\./g, '.')
+    .replace(/\binsect-only motif[^.]*\./g, '.')
+    .replace(/\bwithout\b[^.]*\./g, '.')
+    .replace(/\bexcluding\b[^.]*\./g, '.')
+    .replace(/\b(?:no|not)\s+[^.,;]*(?:banana|bird|butterfl|grape|leaf|leaves|frond|palm|foliage|tropical)[^.]*\./g, '.')
+    .replace(/\bcorrections:[^.]*\./g, '.');
+
+  // Part B — specific prohibited elements (any one = BLOCK).
+  // Word-boundary regex avoids substring false-positives.
+  const partB = [
+    { rx: /\bbanana(?:s|\s+pods?)?\b/, label: 'bananas / banana pods' },
+    { rx: /\bplantains?\b/,            label: 'plantain (banana family)' },
+    { rx: /\bgrapes?\b/,               label: 'grapes' },
+    { rx: /\bbirds?\b/,                label: 'birds' },
+    { rx: /\bbutterfl(?:y|ies)\b/,     label: 'butterflies' },
+  ];
+  for (const e of partB) {
+    if (e.rx.test(p)) {
+      return { verdict: 'BLOCK', reason: `Part B prohibited element: ${e.label}`,
+               suggestion: `Remove "${e.label}" from the prompt. Fliepaper-bugs is insect-only (scarab, luna moth, dragonfly, stag beetle, honeybee, firefly).` };
+    }
+  }
+
+  // Part A — directional foliage with negative space and >1 ink color.
+  // Heuristic flag: any mention of leaves/fronds/palm/foliage/tropical.
+  const foliage = /(\bleaves?\b|fronds?|\bpalm\b|monstera|philodendron|\bfoliage\b|\btropical\b)/.test(p);
+  if (foliage) {
+    return { verdict: 'NEEDS_REVIEW',
+             reason: 'Foliage terms detected. Fliepaper-bugs prompts should be insect-focused; foliage risks Part A directional-leaf coverage.',
+             suggestion: 'Replace foliage with the pattern\'s specified composition (trellis, damask, lacquer panels) or add a tree trunk / clear branch to qualify as Acceptable Tropical Design.' };
+  }
+
+  // Insect-only prompt — clean.
+  return { verdict: 'OK', reason: 'No prohibited elements detected.' };
+}
+
+// ── Compose the generator prompt for a cell from spec composition + colorway.
+function composeCellPrompt(pattern, colorway) {
+  return `${pattern.name} — ${pattern.composition}. Colorway: ${colorway.name} (${colorway.hex}, ${colorway.gucci_ref}); role=${colorway.role}. Seamless tile repeat, luxury wallcovering scale ${pattern.scale}, brand reference Gucci Resort. Insect-only motif (no foliage, no birds, no butterflies, no bananas).`;
+}
+
+// ── Read all 36 cells joined with spoon_all_designs for thumbnails.
+function readCells() {
+  const sql = `
+    SELECT row_to_json(t) FROM (
+      SELECT c.id, c.pattern_slug, c.pattern_name, c.bug, c.scale, c.colorway_slug,
+             c.colorway_name, c.colorway_hex, c.colorway_role, c.gucci_ref, c.status,
+             c.design_id, c.thumbnail_url, c.last_error, c.regen_count, c.generated_at,
+             c.settlement_verdict, c.composition,
+             CASE WHEN d.local_path IS NOT NULL
+                  THEN '/designs/img/' || regexp_replace(d.local_path, '^.*/', '')
+                  ELSE NULL END AS design_image_url,
+             d.is_published AS design_is_published
+        FROM fliepaper_bugs_cells c
+        LEFT JOIN spoon_all_designs d ON d.id = c.design_id
+       WHERE c.collection_slug='fliepaper-bugs'
+       ORDER BY c.pattern_slug, c.colorway_slug
+    ) t;
+  `;
+  const raw = psqlQuery(sql);
+  if (!raw) return [];
+  return raw.split('\n').filter(Boolean).map(line => {
+    try {
+      const r = JSON.parse(line);
+      // Prefer the snapshot thumbnail_url; fall back to design's served image
+      if (!r.thumbnail_url && r.design_image_url) r.thumbnail_url = r.design_image_url;
+      return r;
+    } catch { return null; }
+  }).filter(Boolean);
+}
+
+function mount(app, { isAdmin }) {
+  const path_ = path;
+  const pagesPublic = path_.join(__dirname, '..', 'public', 'collections', 'fliepaper-bugs.html');
+  const pagesAdmin  = path_.join(__dirname, '..', 'public', 'admin',       'fliepaper-bugs.html');
+
+  // ─── HTML — public collection page
+  app.get('/collections/fliepaper-bugs', (_req, res) => {
+    res.setHeader('Cache-Control', 'no-store, must-revalidate');
+    res.sendFile(pagesPublic);
+  });
+
+  // ─── HTML — admin grid (admin-gated)
+  app.get('/admin/fliepaper-bugs', (req, res) => {
+    if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+    res.setHeader('Cache-Control', 'no-store, must-revalidate');
+    res.sendFile(pagesAdmin);
+  });
+
+  // ─── API — collection state (spec + per-cell status)
+  app.get('/api/collections/fliepaper-bugs', (_req, res) => {
+    try {
+      const spec = loadSpec();
+      if (!spec) return res.status(500).json({ error: 'spec.json missing' });
+      const cells = readCells();
+      res.json({ ok: true, spec, cells });
+    } catch (e) {
+      res.status(500).json({ ok: false, error: e.message });
+    }
+  });
+
+  // ─── API — regenerate a cell (admin, settlement-gated, queues yolo agent)
+  app.post('/api/collections/fliepaper-bugs/cell/regenerate', (req, res) => {
+    if (!isAdmin(req)) return res.status(403).json({ ok: false, error: 'admin only' });
+
+    const cellId = parseInt(req.body?.cell_id, 10);
+    if (!Number.isFinite(cellId) || cellId < 1) {
+      return res.status(400).json({ ok: false, error: 'cell_id required' });
+    }
+
+    try {
+      // Look up the cell
+      const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT * FROM fliepaper_bugs_cells WHERE id=${cellId}) t;`);
+      if (!raw) return res.status(404).json({ ok: false, error: 'cell not found' });
+      const cell = JSON.parse(raw);
+
+      const spec = loadSpec();
+      if (!spec) return res.status(500).json({ ok: false, error: 'spec.json missing' });
+      const pattern  = spec.patterns.find(p => p.slug === cell.pattern_slug);
+      const colorway = spec.palette.find(c => c.slug === cell.colorway_slug);
+      if (!pattern || !colorway) {
+        return res.status(500).json({ ok: false, error: 'pattern/colorway mismatch with spec' });
+      }
+
+      // Compose prompt + run settlement gate BEFORE any generation
+      const prompt = composeCellPrompt(pattern, colorway);
+      const verdict = settlementCheck(prompt);
+
+      // Persist the verdict on the cell regardless of outcome
+      psqlQuery(`UPDATE fliepaper_bugs_cells
+                    SET settlement_verdict=${pgEsc(JSON.stringify(verdict))}::jsonb,
+                        updated_at=now()
+                  WHERE id=${cellId};`);
+
+      if (verdict.verdict === 'BLOCK') {
+        psqlQuery(`UPDATE fliepaper_bugs_cells
+                      SET status='SETTLEMENT_BLOCKED',
+                          last_error=${pgEsc('Settlement BLOCK: ' + verdict.reason)},
+                          updated_at=now()
+                    WHERE id=${cellId};`);
+        return res.json({ ok: false, settlement: verdict, message: 'Settlement gate BLOCKED the regenerate request.' });
+      }
+
+      // Queue the yolo-agent / generator. yolo-runner watches
+      // data/fliepaper-bugs/queue/*.json — we drop a job file there.
+      const queueDir = path_.join(__dirname, '..', 'data', 'fliepaper-bugs', 'queue');
+      try { fs.mkdirSync(queueDir, { recursive: true }); } catch {}
+      const jobId = `${Date.now()}_${cellId}`;
+      const jobFile = path_.join(queueDir, `${jobId}.json`);
+      const job = {
+        job_id: jobId,
+        cell_id: cellId,
+        pattern: { slug: pattern.slug, name: pattern.name, bug: pattern.bug, scale: pattern.scale, composition: pattern.composition },
+        colorway: { slug: colorway.slug, name: colorway.name, hex: colorway.hex, role: colorway.role, gucci_ref: colorway.gucci_ref },
+        prompt,
+        settlement: verdict,
+        queued_at: new Date().toISOString(),
+        previous_design_id: cell.design_id || null,
+      };
+      fs.writeFileSync(jobFile, JSON.stringify(job, null, 2));
+
+      // Mark cell PENDING + bump regen_count
+      psqlQuery(`UPDATE fliepaper_bugs_cells
+                    SET status='PENDING',
+                        last_error=NULL,
+                        regen_count=regen_count+1,
+                        updated_at=now()
+                  WHERE id=${cellId};`);
+
+      res.json({
+        ok: true,
+        settlement: verdict,
+        job_file: jobFile,
+        cell_id: cellId,
+        note: 'Queued for yolo-runner. yolo-agent watches data/fliepaper-bugs/queue/. Cell flipped to PENDING.',
+      });
+    } catch (e) {
+      res.status(500).json({ ok: false, error: e.message });
+    }
+  });
+
+  console.log('  Fliepaper-bugs collection mounted (/collections/fliepaper-bugs, /admin/fliepaper-bugs, /api/collections/fliepaper-bugs)');
+}
+
+module.exports = { mount, settlementCheck, composeCellPrompt };

← 8e4960d fliepaper-bugs: collection page + admin grid HTML (placehold  ·  back to Wallco Ai  ·  wallco: fliepaper-bugs tick generator script 7d5c357 →