[object Object]

← back to Wallco Ai

bad_examples registry: dedicated anti-pattern DB ('keep all bad/deleted so we never repeat the error') — backfilled 33,881 bad/deleted designs w/ full prompt+seed+generator provenance; reusable recordBad() helper; logged 11 agent-confirmed repeat-breaks (color-deltaE scan's blind spot)

d042eec882b4cb7167b47621293ac378d552371e · 2026-05-27 09:11:00 -0700 · Steve Abrams

Files touched

Diff

commit d042eec882b4cb7167b47621293ac378d552371e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 09:11:00 2026 -0700

    bad_examples registry: dedicated anti-pattern DB ('keep all bad/deleted so we never repeat the error') — backfilled 33,881 bad/deleted designs w/ full prompt+seed+generator provenance; reusable recordBad() helper; logged 11 agent-confirmed repeat-breaks (color-deltaE scan's blind spot)
---
 scripts/bad-examples-schema.sql | 62 +++++++++++++++++++++++++++++++++++
 scripts/record-bad-example.js   | 71 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 133 insertions(+)

diff --git a/scripts/bad-examples-schema.sql b/scripts/bad-examples-schema.sql
new file mode 100644
index 0000000..e367160
--- /dev/null
+++ b/scripts/bad-examples-schema.sql
@@ -0,0 +1,62 @@
+-- bad_examples — permanent, isolated anti-pattern registry for wallco.ai.
+-- "Keep all bad/deleted items in its own db so we never create the same error."
+-- The negative mirror of the `elements` (good-reference) library.
+--
+-- Design rules:
+--   * ONE row per design_id. defect_tags accumulates every defect signal.
+--   * Stores the PROVENANCE that produced the error (prompt / negative_prompt /
+--     seed / generator / category / dims) so generation can learn the anti-pattern.
+--   * local_path is preserved — the bad FILE is never deleted; this row points to it.
+--   * Lives in dw_unified but is NEVER written to data/designs.json, so catalog
+--     rebuilds / publishes cannot clobber it. Append-and-merge only.
+
+CREATE TABLE IF NOT EXISTS bad_examples (
+  design_id         BIGINT PRIMARY KEY,
+  defect_tags       TEXT[] NOT NULL DEFAULT '{}',   -- {edges_fail, user_removed, ghost, bleed, heal_derivative, repeat_break_lr, repeat_break_tb, ...}
+  severity          INT,                            -- worst known severity 0-3
+  category          TEXT,
+  prompt            TEXT,                           -- the prompt that produced the bad design
+  negative_prompt   TEXT,
+  seed              BIGINT,
+  generator         TEXT,                           -- replicate / comfy / pil-mid-heal / gemini-...
+  width_in          NUMERIC,
+  height_in         NUMERIC,
+  dominant_hex      TEXT,
+  parent_design_id  BIGINT,
+  local_path        TEXT,                           -- preserved bad file (NEVER deleted)
+  scores            JSONB,                          -- lens / structural scores when available
+  notes             TEXT,                           -- original defect detail
+  source            TEXT,                           -- backfill | repeat-break-scan | agent-audit | bulk-reject | edges-agent
+  design_created_at TIMESTAMPTZ,
+  recorded_at       TIMESTAMPTZ DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS bad_examples_defect_idx ON bad_examples USING GIN(defect_tags);
+CREATE INDEX IF NOT EXISTS bad_examples_seed_idx   ON bad_examples(seed);
+CREATE INDEX IF NOT EXISTS bad_examples_cat_idx    ON bad_examples(category);
+CREATE INDEX IF NOT EXISTS bad_examples_gen_idx    ON bad_examples(generator);
+
+-- Backfill from every currently-bad-or-deleted design, merging defect tags.
+INSERT INTO bad_examples
+  (design_id, defect_tags, category, prompt, negative_prompt, seed, generator,
+   width_in, height_in, dominant_hex, parent_design_id, local_path, notes, source, design_created_at)
+SELECT id,
+  ( ARRAY[]::text[]
+    || CASE WHEN user_removed THEN ARRAY['user_removed'] ELSE '{}'::text[] END
+    || CASE WHEN notes ILIKE '%EDGES_FAIL%' THEN ARRAY['edges_fail'] ELSE '{}'::text[] END
+    || CASE WHEN notes ILIKE '%EDGES_SCAN_ERROR%' THEN ARRAY['edges_scan_error'] ELSE '{}'::text[] END
+    || CASE WHEN notes ILIKE '%ghost%' THEN ARRAY['ghost'] ELSE '{}'::text[] END
+    || CASE WHEN notes ILIKE '%bleed%' THEN ARRAY['bleed'] ELSE '{}'::text[] END
+    || CASE WHEN notes ILIKE '%mid_heal%' OR notes ILIKE '%MID_HEALED%' OR generator ILIKE '%heal%'
+            THEN ARRAY['heal_derivative'] ELSE '{}'::text[] END
+  ),
+  category, prompt, negative_prompt, seed, generator,
+  width_in, height_in, dominant_hex, parent_design_id, local_path, notes, 'backfill', created_at
+FROM spoon_all_designs
+WHERE user_removed = TRUE
+   OR notes ILIKE '%EDGES_FAIL%' OR notes ILIKE '%EDGES_SCAN_ERROR%'
+   OR notes ILIKE '%ghost%'      OR notes ILIKE '%bleed%'
+   OR notes ILIKE '%mid_heal%'   OR notes ILIKE '%MID_HEALED%' OR generator ILIKE '%heal%'
+ON CONFLICT (design_id) DO UPDATE
+  SET defect_tags = (SELECT array_agg(DISTINCT t)
+                     FROM unnest(bad_examples.defect_tags || EXCLUDED.defect_tags) AS t),
+      recorded_at = now();
diff --git a/scripts/record-bad-example.js b/scripts/record-bad-example.js
new file mode 100644
index 0000000..45e3189
--- /dev/null
+++ b/scripts/record-bad-example.js
@@ -0,0 +1,71 @@
+#!/usr/bin/env node
+/**
+ * record-bad-example — append/merge a design into the bad_examples anti-pattern
+ * registry, pulling its full provenance (prompt/seed/category/generator/dims)
+ * from spoon_all_designs. NEVER deletes the file; just records the row.
+ *
+ * "Keep all bad/deleted items in its own db so we never create the same error."
+ *
+ * Pipelines (edges-agent, bulk-reject, repeat-break-scan, ghost/bleed guards)
+ * should call recordBad() whenever they flag or delete a design.
+ *
+ * CLI:  node scripts/record-bad-example.js --id 18924 --tags repeat_break_lr --sev 2 \
+ *         --source agent-audit --note "cacti sliced at vertical center" [--scores '{...}']
+ * Lib:  const { recordBad } = require('./record-bad-example');
+ *       recordBad({ designId, tags:['repeat_break_lr'], severity:2, source, note, scores });
+ */
+'use strict';
+const { execFileSync } = require('child_process');
+
+function psql(sql) {
+  return execFileSync('psql', ['-d', 'dw_unified', '-tAc', sql], { encoding: 'utf8' }).trim();
+}
+function lit(v) {
+  if (v === null || v === undefined) return 'NULL';
+  return "'" + String(v).replace(/'/g, "''") + "'";
+}
+function pgArray(arr) {
+  return "ARRAY[" + arr.map(t => lit(t)).join(',') + "]::text[]";
+}
+
+function recordBad({ designId, tags, severity = null, source = 'manual', note = null, scores = null }) {
+  const id = parseInt(designId, 10);
+  if (!Number.isFinite(id)) throw new Error('designId required');
+  if (!Array.isArray(tags) || !tags.length) throw new Error('tags[] required');
+  const scoresLit = scores ? `${lit(JSON.stringify(scores))}::jsonb` : 'NULL';
+  const sevLit = severity == null ? 'NULL' : parseInt(severity, 10);
+  // Pull provenance from spoon_all_designs; merge defect tags on conflict; keep
+  // the worst severity; append the note.
+  const sql = `
+INSERT INTO bad_examples
+  (design_id, defect_tags, severity, category, prompt, negative_prompt, seed, generator,
+   width_in, height_in, dominant_hex, parent_design_id, local_path, scores, notes, source, design_created_at)
+SELECT s.id, ${pgArray(tags)}, ${sevLit}, s.category, s.prompt, s.negative_prompt, s.seed, s.generator,
+       s.width_in, s.height_in, s.dominant_hex, s.parent_design_id, s.local_path, ${scoresLit},
+       ${note ? lit(note) : 's.notes'}, ${lit(source)}, s.created_at
+FROM spoon_all_designs s WHERE s.id=${id}
+ON CONFLICT (design_id) DO UPDATE SET
+  defect_tags = (SELECT array_agg(DISTINCT t) FROM unnest(bad_examples.defect_tags || EXCLUDED.defect_tags) t),
+  severity    = GREATEST(COALESCE(bad_examples.severity,0), COALESCE(EXCLUDED.severity,0)),
+  scores      = COALESCE(EXCLUDED.scores, bad_examples.scores),
+  notes       = COALESCE(EXCLUDED.notes, bad_examples.notes),
+  source      = EXCLUDED.source,
+  recorded_at = now()
+RETURNING design_id;`;
+  const out = psql(sql);
+  return parseInt(out, 10) || id;
+}
+
+module.exports = { recordBad };
+
+if (require.main === module) {
+  const a = process.argv.slice(2);
+  const get = (k) => { const i = a.indexOf('--' + k); return i >= 0 ? a[i + 1] : null; };
+  const tags = (get('tags') || '').split(',').map(s => s.trim()).filter(Boolean);
+  let scores = null; try { if (get('scores')) scores = JSON.parse(get('scores')); } catch {}
+  const id = recordBad({
+    designId: get('id'), tags, severity: get('sev'),
+    source: get('source') || 'manual', note: get('note'), scores,
+  });
+  console.log('recorded bad_example design_id=' + id + ' tags=' + tags.join(','));
+}

← d8d8b1b repeat-break audit: agent vision pass over 36 published desi  ·  back to Wallco Ai  ·  edges-review: sort by status (removed/active first) + dynami 017a711 →