[object Object]

← back to Designerwallcoverings

TK-11073 Wave1: Osborne color: facet from real colorway (title_core − pattern_name), drop AI-palette bare-tags

8950edffb04d4ba5f5bac4ea2decabc935545c99 · 2026-09-01 11:44:08 -0700 · Steve Abrams

Source the color: facet from the real scraped colorway (osborne_catalog.pattern_name
subtracted from title_core) instead of AI vision (e.backgroundColor). Demote AI palette
color-words (e.colors[].name) so they no longer emit as bare tags — palette hues stay in
custom.color_*/color_details metafields. Fall back to NO color tag when the colorway can't
be resolved, never a wrong one. Mirrors the TK-11070 Sanderson fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 8950edffb04d4ba5f5bac4ea2decabc935545c99
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 11:44:08 2026 -0700

    TK-11073 Wave1: Osborne color: facet from real colorway (title_core − pattern_name), drop AI-palette bare-tags
    
    Source the color: facet from the real scraped colorway (osborne_catalog.pattern_name
    subtracted from title_core) instead of AI vision (e.backgroundColor). Demote AI palette
    color-words (e.colors[].name) so they no longer emit as bare tags — palette hues stay in
    custom.color_*/color_details metafields. Fall back to NO color tag when the colorway can't
    be resolved, never a wrong one. Mirrors the TK-11070 Sanderson fix.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/osborne-onboard/build-payloads.mjs | 55 ++++++++++++++++++++++++++++--
 1 file changed, 53 insertions(+), 2 deletions(-)

diff --git a/scripts/osborne-onboard/build-payloads.mjs b/scripts/osborne-onboard/build-payloads.mjs
index 87cc0c0..81040ac 100644
--- a/scripts/osborne-onboard/build-payloads.mjs
+++ b/scripts/osborne-onboard/build-payloads.mjs
@@ -19,8 +19,11 @@
 import fs from 'node:fs';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { execSync } from 'node:child_process';
 import { specMetafields } from '../_spec-to-metafields.mjs';
 
+const DB = 'postgresql:///dw_unified?host=/tmp';
+
 const HERE = path.dirname(fileURLToPath(import.meta.url));
 const OUT = path.join(HERE, 'out');
 const SAMPLE_PRICE = '4.25';
@@ -50,12 +53,46 @@ function bodyHtml(r, e) {
   return `<div class="osborne-product">\n${story}\n</div>`;
 }
 
+// Colorway comes from the REAL scraped name, NEVER AI vision (TK-11073, mirrors TK-11070 Sanderson).
+// Osborne's title_core = "<PatternName> <Colorway>"; the real colorway is title_core with the
+// scraped pattern_name (r.pattern_name, or e.pattern_name) subtracted. Returns null when the
+// colorway can't be resolved confidently → caller emits NO color: facet (never a wrong one).
+function osborneColorway(r) {
+  const decode = s => String(s || '').replace(/&amp;/g, '&').replace(/&#39;/g, "'");
+  const norm = s => titleCase(decode(s)).toLowerCase().replace(/\s+/g, ' ').trim();
+  let tc = decode(r.title_core || '').replace(/^\(limited stock\)\s*/i, '').replace(/^s\s+/i, '').trim();
+  const pat = decode(r.pattern_name || '').trim();
+  if (!tc) return null;
+  if (!pat) return null;                              // no pattern to subtract → can't resolve → skip
+  const tcToks = tc.split(/\s+/), patToks = pat.split(/\s+/);
+  let colorway = null;
+  for (let i = 0; i < tcToks.length; i++) {
+    if (norm(tcToks[i]) === norm(patToks[0])) {
+      let j = 0;
+      while (j < patToks.length && i + j < tcToks.length && norm(tcToks[i + j]) === norm(patToks[j])) j++;
+      colorway = tcToks.slice(i + j).join(' ').trim();
+      break;
+    }
+  }
+  if (colorway == null && norm(tcToks[0]) === norm(patToks[0])) colorway = tcToks.slice(1).join(' ').trim();
+  if (!colorway) return null;
+  colorway = colorway.replace(/\s*\/\s*/g, '/').replace(/\s+/g, ' ').trim();
+  if (!colorway || colorway.length > 40) return null;
+  return colorway;
+}
+
 function tags(r, e) {
   const t = new Set(['Wallcovering', 'Wallcoverings', 'Osborne & Little', 'Osborne & Little Wallcovering', MATERIAL,
                      'Trending Wallcovering Collection 2026']);  // → surfaces in New Arrivals (Steve directive)
   if (r.collection) t.add(r.collection);
-  if (e?.backgroundColor) t.add('color:' + titleCase(e.backgroundColor));
-  for (const c of (e?.colors || [])) if (c?.name) t.add(titleCase(c.name));
+  // color: facet MUST come from the REAL scraped colorway, NOT AI vision (e.backgroundColor / e.colors).
+  // Fall back to NO color tag rather than a wrong one (TK-11073).
+  const cw = osborneColorway(r);
+  if (cw) t.add('color:' + titleCase(cw));
+  // NOTE (TK-11073): AI palette color-words (e.backgroundColor + e.colors[].name — Bone/Dove/Greige/Sky…)
+  // are DEMOTED. They produced stray-colorway pollution (a wrong color: facet + bare palette tags), so
+  // they are no longer emitted as tags. The color: facet above is the single authoritative colorway;
+  // the AI palette hues remain available via the custom.color_*/color_details metafields.
   if (e?.style) t.add(titleCase(e.style));
   if (e?.pattern) t.add(titleCase(e.pattern));
   if (r.limited_stock) t.add('Limited Stock');
@@ -82,12 +119,26 @@ function metafields(r, e) {
   return mf;
 }
 
+// Real scraped pattern_name per mfr_sku from osborne_catalog (Mac2-canonical staging). Used by
+// tags()/osborneColorway() to subtract the pattern from title_core and recover the real colorway.
+function patternMap() {
+  const m = {};
+  try {
+    const raw = execSync(`psql "${DB}" -tAc "select coalesce(json_agg(json_build_object('sku',mfr_sku,'pat',pattern_name))::text,'[]') from osborne_catalog"`,
+      { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
+    for (const r of JSON.parse(raw || '[]')) if (r.sku) m[String(r.sku).toUpperCase()] = r.pat || '';
+  } catch (e) { console.error('patternMap: DB read failed, colorway facet will be skipped —', e.message); }
+  return m;
+}
+
 function main() {
   const ready = load('build-ready.jsonl');
   const enrByMfr = Object.fromEntries(load('enriched.jsonl').filter(x => !x.error).map(x => [x.mfr_sku, x]));
+  const patByMfr = patternMap();
   const out = [];
   let noEnrich = 0;
   for (const r of ready) {
+    r.pattern_name = patByMfr[String(r.mfr_sku).toUpperCase()] || '';   // for osborneColorway()
     const e = enrByMfr[r.mfr_sku];
     if (!e) noEnrich++;
     const sku = `${r.mfr_sku}-OSB`;

← 9b75f40 auto-data-snapshot: 2026-09-01T10:56:56 (8 data files) — scr  ·  back to Designerwallcoverings  ·  TK-11073 Wave1: Osborne LIVE remediation — 29 real-title pro b61983c →