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