← back to Hollywood Import

momentum-feed/build-wallcovering-canary.mjs

90 lines

#!/usr/bin/env node
// TK-00109 Phase-2 Hollywood canary manifest builder (Wallcovering line).
// Picks ~20 genuinely-absent, ready Wallcovering colorways spanning DISTINCT patterns and a
// price range, recomputes hw FRESH = round(list_price × 1.448, 2) (the verified Hollywood
// markup) so a refresh-drifted stored hw_price can never ship, and emits the drip-schema
// manifest that hollywood-create.mjs --manifest=... consumes (DRY-RUN by default).
// $0, reversible (writes local JSON only). Excludes anything already CREATED in the drip audit.
import { createRequire } from 'module';
import fs from 'fs';
const require = createRequire(import.meta.url);
const { Pool } = require('pg');
const pool = new Pool({ connectionString: 'postgresql://dw_admin:DW2024!@127.0.0.1:5432/dw_unified' });
const OUT = new URL('wallcovering-canary.json', import.meta.url).pathname;
const REPORT = new URL('wallcovering-canary-verify.json', import.meta.url).pathname;
const AUDIT = new URL('../hollywood-create-audit.jsonl', import.meta.url).pathname;
const MARKUP = 1.448;
const N = parseInt((process.argv.find(a => a.startsWith('--n=')) || '').split('=')[1] || '20', 10);

// idempotent guard: exclude dw_skus already CREATED by the drip (same source of truth the engine uses)
const created = new Set();
if (fs.existsSync(AUDIT)) for (const l of fs.readFileSync(AUDIT, 'utf8').trim().split('\n').filter(Boolean)) {
  try { const r = JSON.parse(l); if (r.action === 'CREATED' && r.dw) created.add(r.dw); } catch {}
}

async function main() {
  // one representative colorway per DISTINCT pattern (diverse canary), ready + not-yet-live.
  const { rows } = await pool.query(`
    SELECT DISTINCT ON (mc.pattern_name)
           mc.id, mc.dw_sku, mc.momentum_sku, COALESCE(p.hw_title, mc.pattern_name) AS pattern,
           COALESCE(mc.ai_color_name, mc.color_name) AS color,
           mc.list_price, mc.hw_price AS hw_stored, mc.uom, mc.width, mc.image_url,
           mc.content, mc.finish, mc.weight, mc.description, mc.repeat_info, mc.cleaning,
           mc.fire_rating, mc.tags, mc.pl_city_name, p.hw_product_type
      FROM momentum_colorways mc
      LEFT JOIN momentum_golive_prep p ON p.colorway_id = mc.id
     WHERE mc.category = 'Wallcovering'
       AND mc.dw_sku IS NOT NULL AND mc.dw_sku <> ''
       AND mc.list_price > 0
       AND mc.image_url IS NOT NULL AND mc.image_url <> ''
       AND mc.color_name IS NOT NULL
       AND mc.description IS NOT NULL AND mc.description <> ''
       -- data-quality guards (TK-00109 contrarian gate): no opaque internal-code pattern names
       -- (e.g. 'RS00039') surfacing as customer-facing titles, and no acoustic-worded copy on a
       -- wallcovering row (category-mismatched description).
       AND COALESCE(p.hw_title, mc.pattern_name) !~* '^[a-z]{1,3}[0-9]{4,}$'
       AND mc.description !~* 'acoustic panel'
     ORDER BY mc.pattern_name, mc.dw_sku`);

  // drop already-created, then spread across the price range for a stronger price-rule canary
  const pool2 = rows.filter(r => !created.has(r.dw_sku)).sort((a, b) => a.list_price - b.list_price);
  if (pool2.length < N) throw new Error(`only ${pool2.length} candidates, need ${N}`);
  const step = pool2.length / N;
  const picks = Array.from({ length: N }, (_, i) => pool2[Math.floor(i * step)]);

  const items = [], report = [];
  for (const r of picks) {
    const hwFresh = +(Number(r.list_price) * MARKUP).toFixed(2);
    const hwStored = r.hw_stored == null ? null : Number(r.hw_stored);
    const tags = ['Hollywood Wallcoverings', 'Wallcovering', r.color, r.pl_city_name, r.pattern,
      r.momentum_sku, r.fire_rating].filter(Boolean).join(', ');
    items.push({
      id: r.id, dw_sku: r.dw_sku, mfr_sku: r.momentum_sku,
      pattern: r.pattern, color: r.color,
      hw: hwFresh.toFixed(2), uom: r.uom || 'SR', width: r.width || '',
      image: r.image_url, tags,
      content: r.content || '', finish: r.finish || '',
      product_type: r.hw_product_type || 'Wallcovering',
      weight: r.weight || '', description: r.description || '',
      repeat: r.repeat_info || '', cleaning: r.cleaning || '', fire_rating: r.fire_rating || '',
    });
    report.push({
      dw_sku: r.dw_sku, mfr_sku: r.momentum_sku, pattern: r.pattern, color: r.color,
      list_price: Number(r.list_price), hw_fresh: hwFresh, hw_stored: hwStored,
      rule_ok: Math.abs(hwFresh - +(Number(r.list_price) * MARKUP).toFixed(2)) < 0.005,
      above_sample: hwFresh > 4.25,
      drift_vs_stored: hwStored == null ? null : +(hwFresh - hwStored).toFixed(2),
    });
  }
  fs.writeFileSync(OUT, JSON.stringify(items, null, 1));
  fs.writeFileSync(REPORT, JSON.stringify(report, null, 1));
  const prices = report.map(r => r.hw_fresh);
  console.log(`wallcovering-canary.json: ${items.length} items (${new Set(picks.map(p=>p.pattern)).size} distinct patterns).`);
  console.log(`  price span (hw_fresh): $${Math.min(...prices).toFixed(2)} – $${Math.max(...prices).toFixed(2)}`);
  console.log(`  rule_ok: ${report.filter(r=>r.rule_ok).length}/${report.length} · above $4.25 sample: ${report.filter(r=>r.above_sample).length}/${report.length}`);
  console.log(`  drifted vs stored hw: ${report.filter(r=>r.drift_vs_stored && Math.abs(r.drift_vs_stored)>=0.01).length} (fresh recompute wins)`);
  console.log(`  excluded (already CREATED in audit): ${created.size}`);
  await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });