← back to Wallco Ai

scripts/bulk-reject-auto-smart-fix.js

85 lines

#!/usr/bin/env node
// bulk-reject-auto-smart-fix — one-time cleanup of overnight seamless-agent output.
//
// The seamless-agent ran 12+ hours and auto-fired smart-fix on every flagged
// design. Steve rejected the OUTPUT QUALITY ("amateur"). This script:
//   1. Finds every smart-fix event from data/fixes-feed.jsonl in the last N hours
//   2. For each: unpublishes the new_id, republishes the parent (source)
//   3. Tags the new_id with 'etsy-digital-file' (kept for digital-art repurposing)
//   4. Logs every decision to data/fix-decisions.jsonl
//
// Usage:
//   node scripts/bulk-reject-auto-smart-fix.js                 # dry-run, last 12h
//   node scripts/bulk-reject-auto-smart-fix.js --apply         # execute
//   node scripts/bulk-reject-auto-smart-fix.js --apply --hours 24
//   node scripts/bulk-reject-auto-smart-fix.js --apply --kinds smart-fix,regenerate-reverse

'use strict';
const fs   = require('fs');
const path = require('path');
const { psqlExecLocal } = require('../lib/db.js');

const ARGS = process.argv.slice(2);
const arg  = (n, d) => { const i = ARGS.indexOf('--' + n); return i >= 0 ? ARGS[i+1] : d; };
const APPLY  = ARGS.includes('--apply');
const HOURS  = parseInt(arg('hours', '12'), 10);
const KINDS  = (arg('kinds', 'smart-fix') || 'smart-fix').split(',').map(s => s.trim());

const ROOT     = path.join(__dirname, '..');
const FEED     = path.join(ROOT, 'data', 'fixes-feed.jsonl');
const DECISIONS= path.join(ROOT, 'data', 'fix-decisions.jsonl');

function log(m) { console.log(`[${new Date().toISOString()}] ${m}`); }

if (!fs.existsSync(FEED)) { log('no fixes-feed.jsonl — nothing to do'); process.exit(0); }

const cutoffMs = Date.now() - HOURS * 3600 * 1000;
const lines = fs.readFileSync(FEED, 'utf8').split('\n').filter(Boolean);
const targets = [];
for (const line of lines) {
  let row; try { row = JSON.parse(line); } catch { continue; }
  if (!KINDS.includes(row.kind)) continue;
  const ts = row.ts ? Date.parse(row.ts) : 0;
  if (ts < cutoffMs) continue;
  if (!row.new_id || !row.src_id) continue;
  targets.push(row);
}

log(`mode=${APPLY ? 'APPLY' : 'DRY-RUN'} · kinds=${KINDS.join(',')} · hours=${HOURS} · candidates=${targets.length}`);
if (!targets.length) { log('nothing to reject'); process.exit(0); }

if (!APPLY) {
  log('sample (first 5):');
  for (const r of targets.slice(0, 5)) log(`  ${r.kind} #${r.src_id} → #${r.new_id} (${r.ts})`);
  log('re-run with --apply to execute');
  process.exit(0);
}

let ok = 0, errs = 0;
for (const r of targets) {
  try {
    // 1. unpublish new + mark for etsy-digital-file repurposing
    //    PG quirk: `text[] || 'literal'` triggers "malformed array literal"
    //    when the right side isn't explicitly cast as text[]. Use ARRAY[]::text[].
    psqlExecLocal(`UPDATE spoon_all_designs
      SET is_published=FALSE,
          tags = array_remove(COALESCE(tags, ARRAY[]::text[]), 'etsy-digital-file') || ARRAY['etsy-digital-file']::text[]
      WHERE id=${r.new_id};`);
    // 2. republish parent
    psqlExecLocal(`UPDATE spoon_all_designs SET is_published=TRUE WHERE id=${r.src_id};`);
    // 3. audit log
    fs.appendFileSync(DECISIONS, JSON.stringify({
      ts: new Date().toISOString(), new_id: r.new_id, parent: r.src_id,
      verdict: 'reject_bulk_auto_smart_fix',
      tagged_as: 'etsy-digital-file',
      source_event: r,
    }) + '\n');
    ok++;
  } catch (e) {
    errs++;
    log(`  ERR #${r.new_id}: ${e.message.slice(0, 140)}`);
  }
  if ((ok + errs) % 25 === 0) log(`progress ${ok+errs}/${targets.length} · ok=${ok} err=${errs}`);
}
log(`done · ${ok} rejected + tagged etsy-digital-file · ${errs} errors · log=${DECISIONS}`);