← back to Mfr Review Viewer Corruption
scripts/enrich-suggestions.mjs
74 lines
// enrich-suggestions.mjs — fill each active queue row's REAL suggested mfr from
// FileMaker (the code lives in the WALLPAPER master's own record, not dw_unified).
//
// READ-ONLY against FileMaker + Postgres; the ONLY write is data/queue.jsonl (adds
// `suggested_real_mfr` + `suggested_src` + `suggested_keep_rid` per row). Resumable:
// rows already enriched are skipped, so a re-run only fills the gaps. Paced so it
// never hammers FileMaker. The viewer's list renders suggested_real_mfr as it lands.
//
// node scripts/enrich-suggestions.mjs # fill all un-enriched active rows
// node scripts/enrich-suggestions.mjs --limit 50 # sample
// node scripts/enrich-suggestions.mjs --force # re-enrich even filled rows
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dir = dirname(fileURLToPath(import.meta.url));
const QUEUE = join(__dir, '..', 'data', 'queue.jsonl');
// ---- SINGLE-FLIGHT LOCK (Cody, cycle 1): two enrichers writing queue.jsonl at once
// (e.g. ↻ Rebuild clicked twice) would race on the full-file rewrite and lose updates.
// Take a lock; a stale lock (>2h, a crashed run) is reclaimed. Released on exit.
const LOCK = join(__dir, '..', 'data', '.enrich.lock');
if (existsSync(LOCK)) {
let age = Infinity;
try { age = Date.now() - JSON.parse(readFileSync(LOCK, 'utf8')).ts; } catch { age = Infinity; }
if (age < 2 * 60 * 60 * 1000) { console.log('enrich-suggestions: another run holds the lock — exiting (no double-write).'); process.exit(0); }
console.log('enrich-suggestions: reclaiming a stale lock (>2h).');
}
try { writeFileSync(LOCK, JSON.stringify({ pid: process.pid, ts: Date.now() })); } catch { /* best-effort */ }
const releaseLock = () => { try { unlinkSync(LOCK); } catch { /* already gone */ } };
process.on('exit', releaseLock);
process.on('SIGTERM', () => { releaseLock(); process.exit(143); });
process.on('SIGINT', () => { releaseLock(); process.exit(130); });
const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i > -1 ? parseInt(process.argv[i + 1], 10) : 0; })();
const FORCE = process.argv.includes('--force');
const PACE_MS = 350;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const { candidatesForSku } = await import('../lib/candidates.mjs');
function load() {
if (!existsSync(QUEUE)) { console.error('no data/queue.jsonl — run build-queue first'); process.exit(2); }
return readFileSync(QUEUE, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}
function save(rows) { writeFileSync(QUEUE, rows.map((r) => JSON.stringify(r)).join('\n') + '\n'); }
const rows = load();
const todo = rows.filter((r) => FORCE || r.suggested_real_mfr === undefined);
const target = LIMIT ? todo.slice(0, LIMIT) : todo;
console.log(`enrich-suggestions: ${rows.length} rows, ${todo.length} un-enriched, processing ${target.length} (pace ${PACE_MS}ms)`);
let done = 0, withMfr = 0, errs = 0;
for (const r of target) {
try {
const c = await candidatesForSku(r.dw_sku);
const sug = c.suggestion || {};
r.suggested_real_mfr = sug.realMfr || '';
r.suggested_src = sug.realMfr ? 'filemaker-own' : (c.fmError ? 'fm-error' : 'none');
r.suggested_keep_rid = sug.keepRid || '';
if (sug.realMfr) withMfr++;
} catch (e) {
r.suggested_real_mfr = r.suggested_real_mfr || '';
r.suggested_src = 'error';
errs++;
}
done++;
if (done % 25 === 0) { save(rows); console.log(` ${done}/${target.length} · ${withMfr} with real mfr · ${errs} err`); }
await sleep(PACE_MS);
}
save(rows);
const covered = rows.filter((r) => r.suggested_real_mfr).length;
console.log(`DONE: enriched ${done}, ${withMfr} got a real mfr this run. Queue coverage: ${covered}/${rows.length} rows now carry a suggested real mfr.`);