← back to Tk 11331 Exec

d3b_heldreview_prep.mjs

61 lines

#!/usr/bin/env node
// TK-11331 D3b-heldreview — READ-ONLY prep. Recompute held set + verdicts, emit numbers.txt + rows.json.
// No DB writes, no network.
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
const require = createRequire(import.meta.url);
const { Pool } = require('pg');
const HERE = path.dirname(new URL(import.meta.url).pathname);
const TSV = path.join(HERE, 'data', 'momentum_feed_full.tsv');
const pool = new Pool({ host: '/tmp', database: 'dw_unified' });
const norm = s => (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
const fcolor = s => norm(s).replace(/gray/g, 'grey').replace(/[^a-z0-9]/g, '');
const KEEP = new Set(['Wallcovering', 'Acoustic']);
const SETTLEMENT = new Set(['Hula WC', 'Sea Line', 'Palomar', 'Oakstone']);

function readTSV() {
  const lines = fs.readFileSync(TSV, 'utf8').split('\n').filter(Boolean);
  const cols = lines[0].split('\t');
  return lines.slice(1).map(l => { const p = l.split('\t'); const o = {}; cols.forEach((c, i) => o[c] = p[i]); return o; });
}
async function main() {
  const { rows: pr } = await pool.query('SELECT pattern_name, color_name FROM momentum_colorways');
  const stagedPatternNorm = new Set(pr.map(r => norm(r.pattern_name)));
  const stagedPairNorm = new Set(pr.map(r => norm(r.pattern_name) + '' + norm(r.color_name)));
  const patRaw = new Map(); const patFcolors = new Map();
  for (const r of pr) {
    const n = norm(r.pattern_name);
    if (!patRaw.has(n)) patRaw.set(n, new Map());
    patRaw.get(n).set(r.pattern_name, (patRaw.get(n).get(r.pattern_name) || 0) + 1);
    if (!patFcolors.has(n)) patFcolors.set(n, new Set());
    patFcolors.get(n).add(fcolor(r.color_name));
  }
  const canon = n => { const m = patRaw.get(n); return m ? [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].length - b[0].length)[0][0] : null; };
  const feed = readTSV();
  const inscope = feed.filter(r => KEEP.has(r.category_name) && (r.pattern_name || '').trim() && (r.preferred_color_name || '').trim());
  const seen = new Set(); const dedup = [];
  for (const r of inscope) { const k = r.pattern_name + '' + r.preferred_color_name; if (seen.has(k)) continue; seen.add(k); dedup.push(r); }
  const held = dedup.filter(r => stagedPatternNorm.has(norm(r.pattern_name)) && !stagedPairNorm.has(norm(r.pattern_name) + '' + norm(r.preferred_color_name)));
  const rows = [];
  for (const r of held) {
    const np = norm(r.pattern_name); const cp = canon(np);
    const fuzzyDup = patFcolors.get(np)?.has(fcolor(r.preferred_color_name));
    if (fuzzyDup) continue; // DROP-DUP — not staged
    const verdict = cp !== r.pattern_name ? 'DISAMBIGUATE' : 'STAGE-NEW';
    rows.push({
      verdict, stage_pattern: cp, color_name: r.preferred_color_name,
      color_number: r.preferred_color_number || null, number: r.number || null,
      list_price: r.list_price || null, width: (r.base_width || '').trim() || null,
      category: r.category_name, product_line: (r.product_line_code || '').trim() || null,
      uom: (r.uom || '').trim() || null, settlement: SETTLEMENT.has(r.pattern_name.trim()),
    });
  }
  fs.writeFileSync(path.join(HERE, 'data', 'd3b-held-numbers.txt'), rows.map(r => r.number).filter(Boolean).join('\n') + '\n');
  fs.writeFileSync(path.join(HERE, 'data', 'd3b-held-rows.json'), JSON.stringify(rows, null, 2));
  const by = rows.reduce((a, r) => (a[r.verdict] = (a[r.verdict] || 0) + 1, a), {});
  console.log('prep rows:', rows.length, JSON.stringify(by), 'numbers:', rows.filter(r => r.number).length);
  await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });