← back to Hollywood Import
momentum-feed/assign-sku.mjs
119 lines
#!/usr/bin/env node
// Momentum → Hollywood assign-sku: give every unmapped colorway (dw_sku IS NULL) a
// sequential DWHD- SKU, its pattern a fresh CA-coastal collection name (pl_city_name),
// and pl_brand='Hollywood Wallcoverings'. STAGING ONLY — decoupled from the go-live drip,
// which reads the fixed manifest the67_full.json, NOT this table. Reversible.
// Run: NODE_PATH=.../AbramsOS/node_modules node assign-sku.mjs [--commit]
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { Pool } = require('pg');
const pool = new Pool({ connectionString: (process.env.PGCONNSTRING || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified') });
const COMMIT = process.argv.includes('--commit');
// --- TK-10630/TK-10634 GUARD (2026-08-17) ---------------------------------
// "Hollywood Wallcoverings" is a DW BRAND, not an external vendor. Minting a
// DW-vendor-style DWHD-* SKU here is the brand-as-vendor bug that buried each
// product's real code (XWH-/NOC-/HWC-/FRS21-/…). This DWHD scheme is FROZEN
// pending the real-code redesign (TK-10634 #2 — key off the real Momentum
// `number` / manufacturer_sku, not a minted DW code). The kill-switch halts
// the daily drip; this guard also blocks a direct `--commit` re-mint.
import { existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
if (COMMIT && existsSync(`${process.env.HOME}/.dw-fixer-stop`)) {
console.error('[assign-sku] BLOCKED: ~/.dw-fixer-stop present. DWHD minting is FROZEN (brand-as-vendor bug, TK-10634). Remove the kill-switch only after the real-code redesign.');
process.exit(3);
}
// Real CA coastal place names (base pool). Expanded with suffixes if 258+ collision-free needed.
const BASE = `Zuma,Point Dume,Paradise Cove,Leo Carrillo,El Matador,Trancas,Nicholas Canyon,Broad Beach,
Crystal Cove,Corona del Mar,Little Corona,Reef Point,Pelican Point,Abalone Cove,Portuguese Bend,Royal Palms,
Cabrillo Beach,White Point,Point Fermin,Redondo Breakwater,Hermosa Strand,El Porto,Dockweiler,Playa del Rey,
Malaga Cove,Bluff Cove,Lunada Bay,Torrance Beach,Sunset Cliffs,Ocean Beach,La Jolla Shores,Windansea,
Black's Beach,Torrey Pines,Del Mar,Cardiff Reef,Swami's,Moonlight Beach,Ponto,Terramar,Carlsbad State,
San Onofre,Trestles,Church Beach,Cottons Point,Poche,Capistrano Beach,Dana Point,Salt Creek,Monarch Bay,
Three Arch Bay,Woods Cove,Divers Cove,Shaw's Cove,Crescent Bay,Emerald Bay,Cameo Shores,Balboa Peninsula,
Newport Point,Huntington Cliffs,Bolsa Chica,Sunset Beach,Seal Beach,Cabrillo,Refugio,El Capitan,Gaviota,
Arroyo Burro,Butterfly Beach,Miramar,Rincon,Carpinteria,Faria,Emma Wood,Surfers Point,Mondos,Solimar,
Oxnard Shores,Silver Strand,Point Mugu,Sycamore Cove,County Line,Deer Creek,Staircase Beach,Thornhill Broome,
Pfeiffer Beach,Sand Dollar,Willow Creek,Kirk Creek,Limekiln,Partington Cove,McWay Falls,Andrew Molera,
Garrapata,Bixby Bridge,Point Lobos,Carmel River,Monastery Beach,Spanish Bay,Asilomar,Lovers Point,
Moss Landing,Zmudowski,Sunset State,Manresa,Rio Del Mar,Capitola,Pleasure Point,Steamer Lane,Cowell Beach,
Natural Bridges,Wilder Ranch,Scott Creek,Waddell,Ano Nuevo,Pigeon Point,Pescadero,Pomponio,San Gregorio,
Pacifica Pier,Rockaway Beach,Mori Point,Montara,Gray Whale Cove,Devils Slide,Half Moon Bay,Miramar Beach,
Dunes Beach,Poplar Beach,Cowell Ranch,Martins Beach,Ocean Cove,Salt Point,Fort Ross,Timber Cove,
Stillwater Cove,Gerstle Cove,Goat Rock,Wrights Beach,Duncans Landing,Salmon Creek,Bodega Head,Doran Beach,
Dillon Beach,Lawsons Landing,McClures Beach,Limantour,Drakes Beach,Point Reyes,Kehoe Beach,Abbotts Lagoon,
Stinson Beach,Bolinas,Muir Beach,Rodeo Beach,Kirby Cove,Baker Beach,China Beach,Lands End,Ocean Beach SF,
Mavericks,Trinidad State,Moonstone Beach,Clam Beach,Agate Beach,Luffenholtz,College Cove,Houda Point`
.split(',').map(s => s.trim().replace(/\s+/g, ' ')).filter(Boolean);
const SUFFIX = ['Cove','Point','Bluffs','Landing','Reef','Strand','Shores','Head','Bay','Overlook'];
function buildPool(need, used) {
const out = [], seen = new Set([...used].map(s => s.toLowerCase()));
const add = n => { const k = n.toLowerCase(); if (!seen.has(k)) { seen.add(k); out.push(n); } };
for (const n of BASE) { add(n); if (out.length >= need) return out; }
for (const sfx of SUFFIX) for (const n of BASE) { add(`${n.split(' ')[0]} ${sfx}`); if (out.length >= need) return out; }
return out; // (BASE 170 × 10 suffixes ≫ 258; guaranteed enough)
}
async function main() {
const used = (await pool.query(`SELECT DISTINCT pl_city_name c FROM momentum_colorways WHERE pl_city_name IS NOT NULL`))
.rows.map(r => r.c).filter(Boolean);
// --- FORWARD-FIX (DTD verdict C, 2026-08-17): the customer-facing SKU = Hollywood prefix +
// a DETERMINISTIC, OPAQUE transform of the real Momentum number — NOT the raw number and
// NOT a stateful sequential mint. Why C over "HW-<raw number>": the variant SKU propagates
// to the Google/Meta merchant feed, order emails, packing slips, and CSV/EDI exports, so a
// raw Momentum number there is one prefix-strip from a momentumco.com lookup = a private-
// label LEAK ("always pl momentum"). The hash is deterministic (same number → same code,
// idempotent, no counter to drift like DWHD) AND opaque (competitor-safe). The RAW number
// stays ONLY in the internal manufacturer_sku metafield. Prefix + hash length configurable.
const PL_PREFIX = process.env.HW_PL_PREFIX || 'HW';
const HASH_LEN = Number(process.env.HW_HASH_LEN || 12); // hex chars; 12 = 48 bits ≈ collision-free at catalog scale (guarded below)
const plSku = num => `${PL_PREFIX}-${createHash('sha256').update(String(num)).digest('hex').slice(0, HASH_LEN).toUpperCase()}`;
// Deterministic order: WC before Acoustic, then pattern, then colorway.
const { rows } = await pool.query(
`SELECT id, pattern_name, color_name, color_number, category, momentum_sku FROM momentum_colorways
WHERE dw_sku IS NULL ORDER BY category, pattern_name, color_number NULLS LAST, id`);
// A real Momentum number is REQUIRED — no number, no code (never fabricate). Flag + skip.
const noNumber = rows.filter(r => !r.momentum_sku);
const codeable = rows.filter(r => r.momentum_sku);
const patterns = [...new Set(codeable.map(r => r.pattern_name))];
const pool2 = buildPool(patterns.length, used);
if (pool2.length < patterns.length) throw new Error(`pool short: ${pool2.length} < ${patterns.length}`);
const cityOf = new Map(patterns.map((p, i) => [p, pool2[i]]));
const plan = codeable.map(r => ({ id: r.id, dw_sku: plSku(r.momentum_sku), city: cityOf.get(r.pattern_name),
pattern: r.pattern_name, color: r.color_name, cat: r.category, mfr: r.momentum_sku }));
// collision guard: fail loud if the opaque transform ever collides (raise HW_HASH_LEN)
const codes = plan.map(p => p.dw_sku);
if (new Set(codes).size !== codes.length) throw new Error('opaque-SKU hash collision → raise HW_HASH_LEN; aborting');
// ── SKU SHAPE-GUARD (TK-11054) ─────────────────────────────────────────
// Belt-and-suspenders on the STAGING minter. This path mints the forward-fix
// HW-<opaque hash> scheme (NOT the DWHD- sequential — that's frozen). Assert
// fail-loud that NOTHING it writes is a fabricated DW-vendor code or a bare
// Momentum supplier number, which are the two shapes the canary flags. Uses
// the same FABRICATED/BARE regexes as the canary + hollywood-create.mjs.
const FABRICATED = /^DW[A-Z]{1,4}\d?-\d/i, BARE_NUMBER = /^\d{5,}$/;
const bad = plan.filter(p => FABRICATED.test(p.dw_sku) || BARE_NUMBER.test(p.dw_sku));
if (bad.length) throw new Error(`SKU shape-guard: ${bad.length} planned code(s) are fabricated/bare-number (e.g. ${bad.slice(0,3).map(b=>b.dw_sku).join(', ')}) — refusing to mint. Real identity = HW-<hash> / original line code.`);
console.log(`assign-sku (forward-fix ${PL_PREFIX}-<opaque hash>): ${plan.length} colorways / ${patterns.length} patterns. SKIPPED ${noNumber.length} with no momentum_sku. Raw number stays in manufacturer_sku only.`);
console.log(' samples:'); plan.slice(0, 4).forEach(p => console.log(` ${p.dw_sku} ${p.cat} "${p.pattern}" / ${p.color} → ${p.city}`));
if (!COMMIT) { console.log('DRY-RUN — re-run with --commit to write.'); await pool.end(); return; }
const c = await pool.connect();
let done = 0;
try {
await c.query('BEGIN');
for (const p of plan) {
await c.query(
`UPDATE momentum_colorways SET dw_sku=$1, pl_city_name=$2, pl_brand='Hollywood Wallcoverings', updated_at=now()
WHERE id=$3 AND dw_sku IS NULL`, [p.dw_sku, p.city, p.id]);
done++;
}
await c.query('COMMIT');
console.log(`COMMITTED — assigned ${done} ${PL_PREFIX}-<number> private-label SKUs + ${patterns.length} CA-city collections. pl_brand set. (staging only; drip manifest untouched)`);
} catch (e) { await c.query('ROLLBACK'); console.error('ROLLBACK:', e.message); process.exitCode = 1; }
finally { c.release(); await pool.end(); }
}
main().catch(e => { console.error(e); process.exit(1); });