← 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
M public/admin/ghost-review.htmlM scripts/learn-and-purge-flagged.jsM server.js
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 →