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