← 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}`);
})();