[object Object]

← back to Atomic50 Onboard

atomic50: data repair — tin-accurate product_type + descriptions (reversible)

882c948ff0e288da4e5624f094d10f9c35749f07 · 2026-07-13 14:51:31 -0700 · Steve

Files touched

Diff

commit 882c948ff0e288da4e5624f094d10f9c35749f07
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 14:51:31 2026 -0700

    atomic50: data repair — tin-accurate product_type + descriptions (reversible)
---
 .gitignore             |   8 +++
 package-lock.json      | 158 +++++++++++++++++++++++++++++++++++++++++++++++++
 package.json           |   5 ++
 repair-descriptions.js | 128 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 299 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..2df35bf
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,158 @@
+{
+  "name": "atomic50-onboard",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "pg": "^8.22.0"
+      }
+    },
+    "node_modules/pg": {
+      "version": "8.22.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
+      "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.14.0",
+        "pg-pool": "^3.14.0",
+        "pg-protocol": "^1.15.0",
+        "pg-types": "2.2.0",
+        "pgpass": "1.0.5"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.4.0"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+      "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+      "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+      "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
+      "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+      "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..915bdc8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+  "dependencies": {
+    "pg": "^8.22.0"
+  }
+}
diff --git a/repair-descriptions.js b/repair-descriptions.js
new file mode 100644
index 0000000..9b319a7
--- /dev/null
+++ b/repair-descriptions.js
@@ -0,0 +1,128 @@
+#!/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);
+});

(oldest)  ·  back to Atomic50 Onboard  ·  auto-save: 2026-07-13T14:55:38 (5 files) — package.json buil 4ca120d →