← back to Wallco Ai

scripts/audit-ghost-sample.js

114 lines

#!/usr/bin/env node
// audit-ghost-sample — pick a random subset of flagged ghost-layer designs and
// hydrate each row with a resolvable local_path (PG for pg-source rows, derive
// from designs.json for json-source rows). Writes data/ghost-audit-sample.json.
//
// Usage:
//   node scripts/audit-ghost-sample.js                # 50 samples (default)
//   node scripts/audit-ghost-sample.js --n 100        # 100 samples
//   node scripts/audit-ghost-sample.js --seed 42      # reproducible

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const ARGS = process.argv.slice(2);
function arg(name, def) {
  const i = ARGS.indexOf('--' + name);
  return i >= 0 ? ARGS[i + 1] : def;
}
const N    = parseInt(arg('n', '50'), 10);
const SEED = arg('seed', null);

const ROOT     = path.join(__dirname, '..');
const FLAGGED  = path.join(ROOT, 'data', 'ghost-scan-flagged.jsonl');
const OUT      = path.join(ROOT, 'data', 'ghost-audit-sample.json');
const GEN_DIR  = path.join(ROOT, 'data', 'generated');
const DESIGNS  = path.join(ROOT, 'data', 'designs.json');

function loadFlagged() {
  const rows = [];
  for (const line of fs.readFileSync(FLAGGED, 'utf8').split('\n')) {
    if (!line.trim()) continue;
    try { rows.push(JSON.parse(line)); } catch {}
  }
  return rows;
}

// deterministic PRNG when --seed is given (xorshift32)
function rng(seed) {
  if (seed == null) return Math.random;
  let s = (parseInt(seed, 10) >>> 0) || 1;
  return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return ((s >>> 0) % 1e9) / 1e9; };
}

function shuffle(arr, rand) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(rand() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function psqlLookup(ids) {
  if (!ids.length) return {};
  const inList = ids.map(i => parseInt(i, 10)).filter(Number.isFinite).join(',');
  const sql = `SELECT id, local_path FROM spoon_all_designs WHERE id IN (${inList})`;
  const r = spawnSync('psql', ['dw_unified', '-At', '-q'], { input: sql, encoding: 'utf8' });
  if (r.status !== 0) throw new Error(r.stderr || 'psql failed');
  const map = {};
  for (const line of r.stdout.split('\n')) {
    if (!line.trim()) continue;
    const [id, lp] = line.split('|');
    map[id] = lp;
  }
  return map;
}

// JSON-source hydrate: derive disk path from image_url, same as scan script
let designsCache = null;
function jsonLookup(id) {
  if (!designsCache) designsCache = JSON.parse(fs.readFileSync(DESIGNS, 'utf8'));
  const d = designsCache.find(x => x.id === id);
  if (!d) return null;
  if (d.image_url && d.image_url.startsWith('/designs/img/')) {
    const fn = d.image_url.replace(/^\/designs\/img\//, '');
    const p = path.join(GEN_DIR, fn);
    if (fs.existsSync(p)) return p;
  }
  return null;
}

(function main() {
  const all = loadFlagged();
  console.log(`[sample] loaded ${all.length} flagged rows`);
  const rand = rng(SEED);
  const picked = shuffle(all, rand).slice(0, Math.min(N, all.length));

  const pgIds = picked.filter(r => r.source === 'pg').map(r => r.id);
  console.log(`[sample] hydrating ${pgIds.length} pg-source rows via psql`);
  const pgMap = psqlLookup(pgIds);

  const out = [];
  let resolved = 0, missing = 0;
  for (const r of picked) {
    let local_path = null;
    if (r.source === 'pg')   local_path = pgMap[r.id] || null;
    if (r.source === 'json') local_path = jsonLookup(r.id);
    if (local_path && fs.existsSync(local_path)) resolved++; else { missing++; local_path = null; }
    out.push({
      id: r.id,
      category: r.category,
      source: r.source,
      confidence: r.confidence,
      reason: r.reason,
      local_path,
      image_url: r.image_url,
    });
  }
  fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
  console.log(`[sample] wrote ${out.length} rows · resolved=${resolved} missing=${missing}`);
  console.log(`[sample] -> ${OUT}`);
})();