← back to Atomic50 Onboard

repair-descriptions.js

129 lines

#!/usr/bin/env node
/**
 * Atomic 50 Ceilings — data repair (REVERSIBLE, staging table only)
 *
 * 1. product_type: reframe tin tiles + moldings from the wrong "Commercial
 *    Wallcovering" to DW canonical "Architectural Wallcoverings" (the value the
 *    architectural-update-skill migrates all contract/panel lines to; 100 such
 *    products already live). Accessories/Tools keep their install-hardware type.
 * 2. ai_description: rewrite each product to accurately describe a pressed-tin
 *    ceiling tile (T1 tin-plated steel, 25% recycled, unfinished tin + 40+
 *    powder-coat colors) with its REAL motif (from ai_patterns/ai_styles —
 *    Victorian/Damask/Geometric/Art Deco/Floral are accurate to embossed tin)
 *    and applications (ceiling, wall panel, backsplash, wainscot). Word
 *    "wallpaper" is BANNED; tin tiles are NOT "wallcoverings" in copy — they are
 *    pressed-tin ceiling/wall tiles.
 * 3. prices left NULL (quote-only).
 *
 * $0 (local) — deterministic template compose from DB fields, NO paid API/LLM.
 * A full snapshot is taken into atomic50_catalog_bak_repair before any write so
 * the change is trivially reversible.
 */
const { Client } = require('pg');

const DRY = process.argv.includes('--dry-run');

// interior-designer lexicon → tin-accurate phrasing
const MATERIAL =
  'pressed from T1-grade tin-plated steel (25% recycled content) and finished in raw, unfinished tin — with 40+ powder-coat colors available to order';
const APPLICATIONS_TILE =
  'Installs as a pressed-tin ceiling field, an accent wall or wainscot panel, or a backsplash. American-made, ASTM E84 Class A rated.';
const APPLICATIONS_MOLDING =
  'A matching pressed-tin trim that finishes the perimeter of a tin ceiling or wall-panel installation. American-made, ASTM E84 Class A rated.';

// motif phrasing keyed off the real ai_patterns / ai_styles values
function motifPhrase(patterns, styles) {
  const p = (patterns || []).map((s) => String(s).toLowerCase());
  const st = (styles || []).map((s) => String(s).toLowerCase());
  const has = (arr, ...ks) => ks.some((k) => arr.some((v) => v.includes(k)));

  const bits = [];
  if (has(p, 'damask')) bits.push('a classic damask relief with ornate scroll and medallion motifs');
  else if (has(p, 'floral') && has(p, 'geometric')) bits.push('an embossed pattern of floral motifs set within a geometric grid');
  else if (has(p, 'floral')) bits.push('a repeating floral relief in the Victorian tradition');
  else if (has(p, 'geometric')) bits.push('a crisp geometric relief of nested squares and diamonds');
  else if (has(p, 'textured')) bits.push('a hammered, deeply textured surface');
  else bits.push('a decorative embossed relief');

  let styleWord = '';
  if (has(st, 'art deco')) styleWord = 'with a strong Art Deco geometry';
  else if (has(st, 'victorian')) styleWord = 'in an ornate Victorian idiom';
  else if (has(st, 'rococo')) styleWord = 'with Rococo flourish';
  else if (has(st, 'traditional')) styleWord = 'in a traditional register';
  else if (has(st, 'industrial')) styleWord = 'with an industrial edge';

  return styleWord ? `${bits[0]}, ${styleWord}` : bits[0];
}

function tileDescription(row) {
  const motif = motifPhrase(row.ai_patterns, row.ai_styles);
  return `Pressed-tin ceiling tile stamped with ${motif}. Each tile is ${MATERIAL}. The raw tin catches light with a soft metallic sheen and can be left unfinished or powder-coated to a custom color. ${APPLICATIONS_TILE}`;
}

function moldingDescription(row, kind) {
  const motif = motifPhrase(row.ai_patterns, row.ai_styles);
  return `Pressed-tin ${kind.toLowerCase()} stamped with ${motif}. It is ${MATERIAL}. ${APPLICATIONS_MOLDING}`;
}

function accessoryDescription(row) {
  // Strip the banned "wallcovering" framing; accessories are install hardware.
  const name = row.pattern_name || 'Install accessory';
  return `${name} — an installation component for the Atomic 50 pressed-tin ceiling and wall-panel system. Made from durable metal/steel hardware sized to the Atomic 50 tin tile line.`;
}

(async () => {
  const c = new Client({ host: '/tmp', database: 'dw_unified' });
  await c.connect();

  const { rows } = await c.query(
    `SELECT id, product_type, pattern_name, ai_patterns, ai_styles FROM atomic50_catalog ORDER BY id`
  );

  // reversible snapshot
  if (!DRY) {
    await c.query(`DROP TABLE IF EXISTS atomic50_catalog_bak_repair`);
    await c.query(`CREATE TABLE atomic50_catalog_bak_repair AS SELECT * FROM atomic50_catalog`);
    console.log('snapshot -> atomic50_catalog_bak_repair');
  }

  let ptChanged = 0, descChanged = 0;
  for (const r of rows) {
    let newType = r.product_type;
    let newDesc;

    if (r.product_type === 'Commercial Wallcovering') {
      newType = 'Architectural Wallcoverings';
      newDesc = tileDescription(r);
    } else if (/Molding/i.test(r.product_type)) {
      newType = 'Architectural Wallcoverings';
      newDesc = moldingDescription(r, r.product_type);
    } else {
      // Accessory / Tool — keep type, but scrub any leaked wallcovering framing
      newDesc = accessoryDescription(r);
    }

    const willTypeChange = newType !== r.product_type;
    if (willTypeChange) ptChanged++;
    descChanged++;

    if (DRY) {
      if (r.id <= 3 || willTypeChange && ptChanged <= 3)
        console.log(`#${r.id} [${r.product_type} -> ${newType}] ${newDesc.slice(0, 140)}...`);
      continue;
    }

    await c.query(
      `UPDATE atomic50_catalog
         SET product_type=$1, ai_description=$2, updated_at=now()
       WHERE id=$3`,
      [newType, newDesc, r.id]
    );
  }

  console.log(`${DRY ? '[DRY] ' : ''}rows=${rows.length} product_type_changed=${ptChanged} descriptions_rewritten=${descChanged}`);
  await c.end();
})().catch((e) => {
  console.error(e);
  process.exit(1);
});