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