[object Object]

← back to Wallco Ai

feat(regenerate-whole): create sibling design instead of replacing original

631a8c54652a2c4f2f2fdd6b0ae19d84f50f39c3 · 2026-05-24 08:28:05 -0700 · Steve Abrams

Server (/api/design/:id/regenerate-reverse):
- Default mode is now 'sibling' — new design row is INSERTed + published,
  original stays is_published=TRUE. Operator can bulk-delete the original
  via the marquee UI on /admin/ghost-review when they prefer the new one.
- Pass ?replace=1 (or body.replace=1) to keep legacy behavior that
  auto-unpublishes the source. Default is non-destructive.
- Response payload now includes mode + source_still_published.

UI (public/admin/ghost-review.html):
- Status banner reads '+ NEW SIBLING' vs 'REPLACED' depending on response
  mode, and notes whether the original is still published.

Steve 2026-05-24: 'allow a new item to be created when ↻ Reverse-Engineer
+ Regenerate Whole'.

Also fix learn-and-purge-flagged.js: per-query psql wrapper with
50MB maxBuffer + 500-row chunks. lib/db.js psqlQuery() used the default
1MB cap, which blew up at ~100 rows because each DW-spec prompt is 4-7KB.

Files touched

Diff

commit 631a8c54652a2c4f2f2fdd6b0ae19d84f50f39c3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 24 08:28:05 2026 -0700

    feat(regenerate-whole): create sibling design instead of replacing original
    
    Server (/api/design/:id/regenerate-reverse):
    - Default mode is now 'sibling' — new design row is INSERTed + published,
      original stays is_published=TRUE. Operator can bulk-delete the original
      via the marquee UI on /admin/ghost-review when they prefer the new one.
    - Pass ?replace=1 (or body.replace=1) to keep legacy behavior that
      auto-unpublishes the source. Default is non-destructive.
    - Response payload now includes mode + source_still_published.
    
    UI (public/admin/ghost-review.html):
    - Status banner reads '+ NEW SIBLING' vs 'REPLACED' depending on response
      mode, and notes whether the original is still published.
    
    Steve 2026-05-24: 'allow a new item to be created when ↻ Reverse-Engineer
    + Regenerate Whole'.
    
    Also fix learn-and-purge-flagged.js: per-query psql wrapper with
    50MB maxBuffer + 500-row chunks. lib/db.js psqlQuery() used the default
    1MB cap, which blew up at ~100 rows because each DW-spec prompt is 4-7KB.
---
 public/admin/ghost-review.html     |  5 +++--
 scripts/learn-and-purge-flagged.js | 19 ++++++++++++++++---
 server.js                          | 14 +++++++++++---
 3 files changed, 30 insertions(+), 8 deletions(-)

diff --git a/public/admin/ghost-review.html b/public/admin/ghost-review.html
index bd46a57..06c59ad 100644
--- a/public/admin/ghost-review.html
+++ b/public/admin/ghost-review.html
@@ -1964,8 +1964,9 @@
       const dt = ((Date.now() - t0) / 1000).toFixed(1);
       stat.style.borderLeftColor = 'var(--green)';
       stat.style.background = '#eef7ed';
-      stat.innerHTML = `✓ Regenerated in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a><br><span style="font:11px var(--sans); color:var(--faint)">new prompt: ${(j.new_prompt || '').slice(0, 200)}…</span>`;
-      toast(`✓ regenerated → #${j.new_id} (${dt}s)`);
+      const modeLabel = j.source_still_published ? '+ NEW SIBLING' : 'REPLACED';
+      stat.innerHTML = `✓ ${modeLabel} in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original ${j.source_still_published ? '<b>still published</b>' : 'unpublished'}<br><span style="font:11px var(--sans); color:var(--faint)">new prompt: ${(j.new_prompt || '').slice(0, 200)}…</span>`;
+      toast(`✓ ${j.source_still_published ? 'added' : 'regenerated'} → #${j.new_id} (${dt}s)`);
       mainImg.src = `/designs/img/by-id/${j.new_id}?regen=1`;
       // Save a tp_other label to record the action
       const verdict = 'tp_other';
diff --git a/scripts/learn-and-purge-flagged.js b/scripts/learn-and-purge-flagged.js
index 561bf25..241b09a 100755
--- a/scripts/learn-and-purge-flagged.js
+++ b/scripts/learn-and-purge-flagged.js
@@ -23,8 +23,20 @@
 
 const fs   = require('fs');
 const path = require('path');
+const { execSync } = require('child_process');
 const { psqlQuery } = require('../lib/db.js');
 
+// Per-query psql wrapper with a big maxBuffer — lib/db.js psqlQuery uses
+// the default 1MB cap, which blows up at ~100 rows when each row has a
+// 4-7KB DW-spec prompt string. We need 50MB to safely fetch chunks of 500.
+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 psqlBig(sql) {
+  return execSync(PSQL_CMD, { input: sql, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }).trim();
+}
+
 const ROOT       = path.join(__dirname, '..');
 const FLAGGED    = path.join(ROOT, 'data', 'ghost-scan-flagged.jsonl');
 const LEARN_LOG  = path.join(ROOT, 'data', 'bad-aesthetic-patterns.jsonl');
@@ -66,10 +78,11 @@ function main() {
   console.log(`[learn-purge] flagged.jsonl unique ids: ${ids.length}`);
 
   // Pull prompt + motifs + local_path + is_published for every id, chunked
-  // so the SQL stays under the command-line length cap.
+  // small (500/round) so each psql response stays under ~30MB even when
+  // every row carries a 4-7KB DW-spec prompt string.
   const rowsById = new Map();
-  for (const chunk of chunked(ids, 1000)) {
-    const raw = psqlQuery(
+  for (const chunk of chunked(ids, 500)) {
+    const raw = psqlBig(
       `SELECT row_to_json(t) FROM (
          SELECT id, category, prompt, motifs, local_path, is_published, user_removed
          FROM spoon_all_designs WHERE id IN (${chunk.join(',')})
diff --git a/server.js b/server.js
index 60bf8fe..cbac942 100644
--- a/server.js
+++ b/server.js
@@ -7281,10 +7281,18 @@ RETURNING id`);
     const newId = parseInt(ins, 10);
     if (!newId) return res.status(500).json({ error: 'failed to insert new row' });
     psqlExecLocal(`UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${newId}', is_published=TRUE WHERE id=${newId}`);
-    psqlExecLocal(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${id}`);
+    // The new design is a SIBLING of the original, not a replacement. The
+    // original stays published — the operator can bulk-delete it later via
+    // the marquee UI on /admin/ghost-review if they pick the new one over
+    // the original. Earlier behavior auto-unpublished the source; that made
+    // Regenerate Whole feel like a destructive replace instead of an "add
+    // new item" action (Steve's 2026-05-24 ask). Pass ?replace=1 to keep
+    // the old behavior for callers that still want it.
+    const replaceMode = String(req.query?.replace || req.body?.replace || '') === '1';
+    if (replaceMode) psqlExecLocal(`UPDATE spoon_all_designs SET is_published=FALSE WHERE id=${id}`);
     const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
-    appendFixEvent({ kind: 'regenerate-reverse', src_id: id, new_id: newId, elapsed_s: elapsed, prompt_origin: promptOrigin, new_prompt: newPrompt.slice(0, 400) });
-    return res.json({ ok: true, new_id: newId, new_prompt: newPrompt, prompt_origin: promptOrigin, elapsed_s: elapsed, src_id: id });
+    appendFixEvent({ kind: 'regenerate-reverse', src_id: id, new_id: newId, elapsed_s: elapsed, prompt_origin: promptOrigin, new_prompt: newPrompt.slice(0, 400), mode: replaceMode ? 'replace' : 'sibling' });
+    return res.json({ ok: true, new_id: newId, new_prompt: newPrompt, prompt_origin: promptOrigin, elapsed_s: elapsed, src_id: id, mode: replaceMode ? 'replace' : 'sibling', source_still_published: !replaceMode });
   } catch (e) {
     console.error('[regenerate-reverse]', e);
     return res.status(500).json({ error: e.message });

← b362f0f feat(scripts): learn-and-purge-flagged.js — headless equival  ·  back to Wallco Ai  ·  fix(ghost-detector): vendor-category filter on PG source pat 67eac74 →