[object Object]

← back to All Designerwallcoverings

Integrate 5 vendor live-stock adapters: enable Koroseal (liveness), dim Designtex/Schumacher/Fabricut/Contrado honestly

1aabc95f6403ddac1c1bf06c74dd34d1f59b343d · 2026-07-07 16:44:27 -0700 · Steve

- Register koroseal/designtex/schumacher/fabricut/contrado in adapters/index.js
- resolve.js: shape-general template-learning fallback (additive; only ADDS matches after a product_url miss, needs a recovered mfr_sku)
- koroseal.js: recover mfr_sku from shopify_products before URL reconstruction (server passes dw_sku, reconstruction keys on mfr code)
- build-coverage.js: LIVENESS_ADAPTERS (Koroseal, mfr-key resolvability gate, honest Available/Discontinued label); WIRED_DIM (Schumacher quote-only / Fabricut too-small / Contrado made-to-order); Designtex honest-dim (grid rows carry no SKU key)
- live count 12 -> 13 (+Koroseal); all 12 prior live vendors retained (no regression)

Files touched

Diff

commit 1aabc95f6403ddac1c1bf06c74dd34d1f59b343d
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 16:44:27 2026 -0700

    Integrate 5 vendor live-stock adapters: enable Koroseal (liveness), dim Designtex/Schumacher/Fabricut/Contrado honestly
    
    - Register koroseal/designtex/schumacher/fabricut/contrado in adapters/index.js
    - resolve.js: shape-general template-learning fallback (additive; only ADDS matches after a product_url miss, needs a recovered mfr_sku)
    - koroseal.js: recover mfr_sku from shopify_products before URL reconstruction (server passes dw_sku, reconstruction keys on mfr code)
    - build-coverage.js: LIVENESS_ADAPTERS (Koroseal, mfr-key resolvability gate, honest Available/Discontinued label); WIRED_DIM (Schumacher quote-only / Fabricut too-small / Contrado made-to-order); Designtex honest-dim (grid rows carry no SKU key)
    - live count 12 -> 13 (+Koroseal); all 12 prior live vendors retained (no regression)
---
 data/live-stock-coverage.json | 49 ++++++++++++++--------------
 scripts/adapters/index.js     |  5 +++
 scripts/adapters/koroseal.js  | 28 +++++++++++++---
 scripts/adapters/resolve.js   | 29 +++++++++++++++--
 scripts/build-coverage.js     | 76 +++++++++++++++++++++++++++++++++++++++++--
 5 files changed, 153 insertions(+), 34 deletions(-)

diff --git a/data/live-stock-coverage.json b/data/live-stock-coverage.json
index fd145dd..e8b57f3 100644
--- a/data/live-stock-coverage.json
+++ b/data/live-stock-coverage.json
@@ -1,14 +1,14 @@
 {
-  "generated_at": "2026-07-07T20:35:39.895Z",
+  "generated_at": "2026-07-07T23:43:44.314Z",
   "generator": "scripts/build-coverage.js",
   "total_display_vendors": 254,
   "counts": {
     "PORTAL_CREDS": 6,
     "PUBLIC_STOCK": 24,
-    "PORTAL_NO_CREDS": 17,
-    "QUOTE_ONLY": 35,
-    "NO_STOCK": 172,
-    "live_capable": 12
+    "PORTAL_NO_CREDS": 15,
+    "QUOTE_ONLY": 36,
+    "NO_STOCK": 173,
+    "live_capable": 13
   },
   "vendors": {
     "1838 Wallcoverings": {
@@ -512,17 +512,15 @@
       "active": 0
     },
     "Contrado": {
-      "tier": "PUBLIC_STOCK",
+      "tier": "NO_STOCK",
       "live_capable": false,
-      "adapter": "schema-public",
-      "reason": "adapter wired (schema-public) but display SKUs don't align with the contrado_catalog catalog (0 of 15 active resolvable) — needs SKU remap",
+      "adapter": "contrado",
+      "reason": "made to order — no live stock signal",
       "catalog_table": "contrado_catalog",
       "backing": "Contrado",
       "match": "exact",
       "products": 19,
-      "active": 15,
-      "resolvable_active": 0,
-      "resolvable_pct": 0
+      "active": 15
     },
     "Coordonn��": {
       "tier": "NO_STOCK",
@@ -630,14 +628,12 @@
       "tier": "PUBLIC_STOCK",
       "live_capable": false,
       "adapter": "schema-public",
-      "reason": "adapter wired (schema-public) but display SKUs don't align with the designtex_catalog catalog (0 of 288 active resolvable) — needs SKU remap",
+      "reason": "Designtex publishes login-free schema.org availability, but its grid products carry no dw_sku/mfr_sku (all empty) — no SKU key exists to route a live check to the right PDP; enabling needs the DWDX dw_sku backfilled into shopify_products (gated data write)",
       "catalog_table": "designtex_catalog",
       "backing": "Designtex",
       "match": "exact",
       "products": 317,
-      "active": 288,
-      "resolvable_active": 0,
-      "resolvable_pct": 0
+      "active": 288
     },
     "Dolce & Gabbana": {
       "tier": "QUOTE_ONLY",
@@ -840,8 +836,8 @@
     "Fabricut": {
       "tier": "PORTAL_NO_CREDS",
       "live_capable": false,
-      "adapter": null,
-      "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+      "adapter": "fabricut",
+      "reason": "price-oriented trade portal + catalog too small (needs ≥5 aligned SKUs)",
       "catalog_table": "fabricut_catalog",
       "backing": "Fabricut",
       "match": "exact",
@@ -1387,15 +1383,18 @@
       "active": 2
     },
     "Koroseal": {
-      "tier": "PORTAL_NO_CREDS",
-      "live_capable": false,
-      "adapter": null,
-      "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+      "tier": "PUBLIC_STOCK",
+      "live_capable": true,
+      "adapter": "koroseal",
+      "reason": "Available / Discontinued (product liveness — spec/sample line, no inventory count)",
       "catalog_table": "koroseal_catalog",
       "backing": "Koroseal",
       "match": "exact",
       "products": 2548,
-      "active": 2351
+      "active": 2351,
+      "resolvable_active": 2351,
+      "resolvable_pct": 100,
+      "verified": "wired — not yet live-verified"
     },
     "Kravet": {
       "tier": "PORTAL_NO_CREDS",
@@ -2390,10 +2389,10 @@
       "active": 966
     },
     "Schumacher": {
-      "tier": "PORTAL_NO_CREDS",
+      "tier": "QUOTE_ONLY",
       "live_capable": false,
-      "adapter": null,
-      "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+      "adapter": "schumacher",
+      "reason": "quote-only trade line — portal exposes no live stock signal",
       "catalog_table": "schumacher_catalog",
       "backing": "Schumacher",
       "match": "exact",
diff --git a/scripts/adapters/index.js b/scripts/adapters/index.js
index c9720f9..26f1875 100644
--- a/scripts/adapters/index.js
+++ b/scripts/adapters/index.js
@@ -18,6 +18,11 @@ const ADAPTERS = {
   'shopify-public': require('./shopify-public'),
   'woo-public': require('./woo-public'),
   'schema-public': require('./schema-public'), // login-free schema.org/Magento availability
+  koroseal: require('./koroseal'),   // login-free Available/Discontinued liveness (spec/sample line — no inventory count)
+  designtex: require('./designtex'), // login-free schema.org availability (WIRED; parked — grid rows carry no SKU key to route)
+  schumacher: require('./schumacher'), // vault-portal (WIRED; parked/dim — quote-only line, portal exposes no live stock signal)
+  fabricut: require('./fabricut'),   // vault-portal (WIRED; parked/dim — price-oriented, no stock field; catalog too small)
+  contrado: require('./contrado'),   // made-to-order (WIRED; parked/dim — print-on-demand, no live stock signal exists)
 };
 
 function get(key) { return ADAPTERS[key] || null; }
diff --git a/scripts/adapters/koroseal.js b/scripts/adapters/koroseal.js
index 09d83c9..bf85af2 100644
--- a/scripts/adapters/koroseal.js
+++ b/scripts/adapters/koroseal.js
@@ -104,14 +104,34 @@ async function matchThumbUrl(page, sku) {
   }).catch(() => null);
 }
 
+// Recover the SKU's Koroseal mfr code from shopify_products. The grid/server passes the DISPLAY sku
+// (usually the dw_sku, e.g. "DWK-29052"), but the URL reconstruction below keys on the mfr code
+// (e.g. "TAK-EA02-01" / "SE81-21") — the collection prefix maps + thumb-strip SKU text are mfr codes,
+// not DWK dw_skus. So bridge dw_sku → mfr_sku before reconstructing; fall back to the raw sku if the
+// grid row itself already carries the mfr code.
+async function recoverMfrSku(pool, sku) {
+  try {
+    const { rows } = await pool.query(
+      `SELECT mfr_sku FROM shopify_products
+        WHERE upper(coalesce(dw_sku,''))=upper($1) OR upper(coalesce(variant_sku,''))=upper($1)
+           OR upper(coalesce(sku,''))=upper($1) LIMIT 1`, [sku]);
+    if (rows[0] && rows[0].mfr_sku) return String(rows[0].mfr_sku).trim();
+  } catch { /* optional */ }
+  return null;
+}
+
 // Resolve the grid SKU → its koroseal.com PDP. Catalog join first (works once koroseal_catalog is
-// backfilled); on miss, live-reconstruct via the collection/thumb walk described above.
+// backfilled); on miss, live-reconstruct via the collection/thumb walk described above — keyed on the
+// recovered MFR code (not the DWK dw_sku the server passes).
 async function resolveKorosealUrl(page, pool, catalogTable, sku) {
   const r = await resolveCatalogUrl(pool, catalogTable || 'koroseal_catalog', sku).catch(() => ({ error: 'resolve failed' }));
   if (r && r.product_url) return { url: r.product_url, via: 'catalog' };
 
-  // Fallback: reconstruct from the SKU by mining the live collection/search page's thumbs.
-  for (const cp of korosealCollectionPages(sku)) {
+  // The reconstruction key is the mfr code — recover it from shopify_products, else use the raw sku.
+  const mfr = (await recoverMfrSku(pool, sku)) || sku;
+
+  // Fallback: reconstruct from the mfr code by mining the live collection/search page's thumbs.
+  for (const cp of korosealCollectionPages(mfr)) {
     try {
       await page.goto(cp, { waitUntil: 'domcontentloaded', timeout: 45000 });
       await page.waitForTimeout(2500);
@@ -119,7 +139,7 @@ async function resolveKorosealUrl(page, pool, catalogTable, sku) {
       if (/\/products\/specialty\/koroseal-specialty\/[^/]+\/[^/]+/.test(page.url()) && !/page-not-found|404/i.test(await page.content().catch(() => ''))) {
         return { url: page.url(), via: 'reconstruct-direct' };
       }
-      const href = await matchThumbUrl(page, sku);
+      const href = await matchThumbUrl(page, mfr);
       if (href) return { url: href.startsWith('http') ? href : `${BASE}${href.startsWith('/') ? '' : '/'}${href}`, via: 'reconstruct-thumb' };
     } catch { /* try next candidate */ }
   }
diff --git a/scripts/adapters/resolve.js b/scripts/adapters/resolve.js
index 6f7bbcf..d0ea4db 100644
--- a/scripts/adapters/resolve.js
+++ b/scripts/adapters/resolve.js
@@ -18,6 +18,7 @@ async function resolveCatalogUrl(pool, table, gridSku) {
 
   // recover the display row's dw_sku + mfr_sku from the grid sku
   const keys = new Set([String(gridSku).toUpperCase()]);
+  let recoveredMfr = null; // the SKU's own mfr code, kept for the template-learning fallback below
   try {
     const { rows } = await pool.query(
       `SELECT dw_sku, mfr_sku FROM shopify_products
@@ -27,6 +28,7 @@ async function resolveCatalogUrl(pool, table, gridSku) {
       if (rows[0].dw_sku) keys.add(String(rows[0].dw_sku).toUpperCase());
       if (rows[0].mfr_sku) {
         const raw = String(rows[0].mfr_sku).toUpperCase();
+        recoveredMfr = String(rows[0].mfr_sku).trim();
         keys.add(raw);
         // Some vendors carry a COMPOUND display mfr_sku that prepends the real mfr code to the
         // pattern+color (e.g. Osborne & Little "W7813-02-Villa Como-Aqua"), while the vendor catalog
@@ -47,8 +49,31 @@ async function resolveCatalogUrl(pool, table, gridSku) {
   const sel = ['product_url', cols.has('mfr_sku') ? 'mfr_sku' : "'' AS mfr_sku", cols.has('handle') ? 'handle' : "'' AS handle"].join(', ');
   const { rows } = await pool.query(
     `SELECT ${sel} FROM "${table}" WHERE (${wh.join(' OR ')}) AND product_url IS NOT NULL AND product_url<>'' LIMIT 1`, [keyArr]);
-  if (!rows.length) return { error: 'SKU not found in this vendor catalog' };
-  return { product_url: rows[0].product_url, mfr_sku: rows[0].mfr_sku || null, handle: rows[0].handle || null };
+  if (rows.length) return { product_url: rows[0].product_url, mfr_sku: rows[0].mfr_sku || null, handle: rows[0].handle || null };
+
+  // TEMPLATE-LEARNING FALLBACK (shape-general, additive) — reached ONLY when the direct join above
+  // found no catalog row carrying a product_url for this SKU. Some vendor catalogs populate
+  // product_url on only SOME rows while the URL shape embeds each row's own mfr_sku (e.g. a Magento
+  // /products/<code> path). When this SKU's own row has an empty/missing product_url, we LEARN that
+  // shape from any sibling row whose product_url contains its own mfr_sku, then substitute this SKU's
+  // recovered mfr code. Because it runs only after a miss and needs a real recovered mfr_sku, it can
+  // ONLY ADD a match — it never changes a vendor whose SKUs already resolve. Requires the grid row to
+  // carry an mfr_sku (vendors whose grid rows have no SKU key are still correctly unresolved).
+  if (recoveredMfr && cols.has('mfr_sku')) {
+    try {
+      const { rows: tpl } = await pool.query(
+        `SELECT mfr_sku, product_url FROM "${table}"
+          WHERE product_url IS NOT NULL AND product_url<>'' AND mfr_sku IS NOT NULL AND mfr_sku<>''
+            AND position(mfr_sku in product_url) > 0 LIMIT 50`);
+      for (const t of tpl) {
+        const code = String(t.mfr_sku).trim();
+        if (code && String(t.product_url).includes(code)) {
+          return { product_url: String(t.product_url).split(code).join(recoveredMfr), mfr_sku: recoveredMfr, handle: null, synthesized: true };
+        }
+      }
+    } catch { /* template sample optional — fall through to the honest not-found */ }
+  }
+  return { error: 'SKU not found in this vendor catalog' };
 }
 
 // nav-menu strip regex — vendor PDPs often embed "Discontinued List" / "Sale List" /
diff --git a/scripts/build-coverage.js b/scripts/build-coverage.js
index e8a4152..1281a7b 100644
--- a/scripts/build-coverage.js
+++ b/scripts/build-coverage.js
@@ -87,6 +87,12 @@ const SCHEMA_PUBLIC = new Set([
 //   retailers). No public stock signal exists to read, so a live check would only ever return unknown.
 const SCHEMA_PARSE_PENDING = new Map([
   ['as creation', 'AS Creation’s public site is a pattern catalog / where-to-buy reference (empty schema.org Offer, no cart, no stock markup) — it publishes no live stock; sold through retailers'],
+  // Designtex DOES publish login-free schema.org/Magento availability (the adapter reads it fine), but
+  // every grid Designtex product carries an EMPTY dw_sku/mfr_sku/variant_sku/sku — the card has no SKU
+  // key at all, so a live check can't be routed to the right PDP and the products API serializes its
+  // sku as null. The fix is a shopify_products backfill of the DWDX dw_sku (present in the handle,
+  // e.g. …-dwdx-220315, and in designtex_catalog.dw_sku) — a gated data write, out of scope here.
+  ['designtex', 'Designtex publishes login-free schema.org availability, but its grid products carry no dw_sku/mfr_sku (all empty) — no SKU key exists to route a live check to the right PDP; enabling needs the DWDX dw_sku backfilled into shopify_products (gated data write)'],
 ]);
 
 // Portals that require a login AND publish only PRICE (a wholesale price sheet / SFTP feed), never
@@ -107,6 +113,34 @@ const VAULT_PORTAL_ADAPTERS = new Map([
     reason: 'Thibaut trade portal — creds referenced in place from the dw-price-stock vault (live stock behind trade login)' }],
 ]);
 
+// LIVENESS adapters — login-free vendors whose ONLY honest "live" signal is product liveness
+// (does the vendor PDP still resolve → Available, vs 301/404/redirect-to-category → Discontinued),
+// NOT a stock count or a price. Koroseal (koroseal.com) is a commercial spec/sample catalog with no
+// cart, no public price, and no inventory field — so it reports Available/Discontinued and NEVER a
+// price. It resolves each grid SKU to its PDP by RECONSTRUCTING the URL from the mfr code (the
+// koroseal.js collection-prefix + live thumb match), so its resolvability is governed by mfr-key
+// presence (2351/2351 active carry a real mfr code), not by koroseal_catalog.product_url (which is
+// sparse — only the digital-lab line). Routed BEFORE the login_url branch so it isn't mis-dimmed as
+// PORTAL_NO_CREDS. Steve-approved 2026-07-07.
+const LIVENESS_ADAPTERS = new Map([
+  ['koroseal', { adapter: 'koroseal', catalog_table: 'koroseal_catalog', resolves_by: 'mfr',
+    reason: 'Available / Discontinued (product liveness — spec/sample line, no inventory count)' }],
+]);
+
+// WIRED-but-DIM trade/quote lines whose adapters are registered (scripts/adapters/) but which expose
+// NO honest live-stock signal, so coverage keeps them dim with a specific, accurate reason rather than
+// burning a metered session to read nothing. (Schumacher/Fabricut/Contrado authoring pass 2026-07-07.)
+const WIRED_DIM = new Map([
+  ['schumacher', { tier: 'QUOTE_ONLY', adapter: 'schumacher',
+    reason: 'quote-only trade line — portal exposes no live stock signal' }],
+  ['fabricut', { tier: 'PORTAL_NO_CREDS', adapter: 'fabricut',
+    reason: 'price-oriented trade portal + catalog too small (needs ≥5 aligned SKUs)' }],
+  ['fabricut stroheim', { tier: 'PORTAL_NO_CREDS', adapter: 'fabricut',
+    reason: 'price-oriented trade portal + catalog too small (needs ≥5 aligned SKUs)' }],
+  ['contrado', { tier: 'NO_STOCK', adapter: 'contrado',
+    reason: 'made to order — no live stock signal' }],
+]);
+
 const QUOTE_ONLY = new Set([
   'dedar', 'de gournay', 'de gournay murals', 'fromental', 'gracie', 'iksel', 'zuber',
   'porter teleo', 'holland and sherry', 'holland & sherry', 'clarence house', 'fortuny',
@@ -201,6 +235,22 @@ async function main() {
         reason: 'WallQuest trade portal (creds on file)', catalog_table: 'wallquest_catalog' };
     if (QUOTE_ONLY.has(v)) return { tier: 'QUOTE_ONLY', live_capable: false, adapter: null,
       reason: 'vendor does not publish live stock (to-the-trade / quote-only)' };
+    // WIRED-but-DIM registered adapters with no honest live-stock signal (Schumacher/Fabricut/Contrado).
+    // Kept dim on purpose — the adapter exists and is registered, but coverage will not ENABLE a
+    // metered check because the vendor exposes nothing live to read. Precedes backing/portal branches.
+    if (WIRED_DIM.has(v)) {
+      const w = WIRED_DIM.get(v);
+      return { tier: w.tier, live_capable: false, adapter: w.adapter, reason: w.reason,
+        catalog_table: (backing && backing.catalog_table) || null };
+    }
+    // LIVENESS adapters (Koroseal) — login-free Available/Discontinued via URL reconstruction from the
+    // mfr code. Routed before the login_url branch so a spec-catalog login_url doesn't mis-dim it.
+    if (LIVENESS_ADAPTERS.has(v)) {
+      const m = LIVENESS_ADAPTERS.get(v);
+      return { tier: 'PUBLIC_STOCK', live_capable: true, adapter: m.adapter,
+        reason: m.reason, catalog_table: m.catalog_table || (backing && backing.catalog_table) || null,
+        resolves_by: m.resolves_by };
+    }
     if (!backing) return { tier: 'NO_STOCK', live_capable: false, adapter: null,
       reason: 'no live stock source identified for this line (private-label aggregate or unmapped)' };
 
@@ -284,6 +334,21 @@ async function main() {
     } catch { return { active: 0, resolvable: 0 }; }
   }
 
+  // Resolvability gate for LIVENESS adapters that resolve via URL RECONSTRUCTION from the mfr code
+  // (Koroseal) rather than a catalog product_url join. Here "resolvable" = active grid rows carrying a
+  // usable mfr code (the reconstruction key). Koroseal's koroseal_catalog.product_url is sparse (only
+  // the digital-lab line), so the product_url gate would wrongly dim it; the honest measure is mfr-key
+  // presence — 2351/2351 active Koroseal SKUs have one.
+  async function mfrKeyCount(vendor) {
+    try {
+      const { rows } = await pool.query(
+        `SELECT count(*) FILTER (WHERE upper(status)='ACTIVE')::int AS active,
+                count(*) FILTER (WHERE upper(status)='ACTIVE' AND coalesce(mfr_sku,'') <> '')::int AS resolvable
+           FROM shopify_products WHERE vendor = $1`, [vendor]);
+      return rows[0];
+    } catch { return { active: 0, resolvable: 0 }; }
+  }
+
   const vendors = {};
   const counts = { PORTAL_CREDS: 0, PUBLIC_STOCK: 0, PORTAL_NO_CREDS: 0, QUOTE_ONLY: 0, NO_STOCK: 0, live_capable: 0 };
   for (const dv of displayVendors) {
@@ -295,14 +360,19 @@ async function main() {
       backing: backing ? backing.vendor_name : null, match: method,
       products: dv.total, active: dv.active,
     };
-    // Resolvability gate: only ENABLE a wired vendor whose display SKUs actually align with its catalog.
+    // Resolvability gate: only ENABLE a wired vendor whose display SKUs actually resolve to a PDP.
+    // Reconstruction adapters (Koroseal) are gated on mfr-key presence; all others on catalog product_url join.
     if (entry.live_capable && entry.adapter) {
-      const { active, resolvable } = await resolvableCount(dv.vendor, entry.catalog_table);
+      const { active, resolvable } = c.resolves_by === 'mfr'
+        ? await mfrKeyCount(dv.vendor)
+        : await resolvableCount(dv.vendor, entry.catalog_table);
       entry.resolvable_active = resolvable;
       entry.resolvable_pct = active ? Math.round((resolvable / active) * 100) : 0;
       if (!(resolvable >= 5 && entry.resolvable_pct >= 20)) {
         entry.live_capable = false;
-        entry.reason = `adapter wired (${entry.adapter}) but display SKUs don't align with the ${entry.catalog_table} catalog (${resolvable} of ${active} active resolvable) — needs SKU remap`;
+        entry.reason = c.resolves_by === 'mfr'
+          ? `adapter wired (${entry.adapter}) but too few Koroseal SKUs carry an mfr code to reconstruct a PDP (${resolvable} of ${active} active)`
+          : `adapter wired (${entry.adapter}) but display SKUs don't align with the ${entry.catalog_table} catalog (${resolvable} of ${active} active resolvable) — needs SKU remap`;
       }
     }
     if (entry.live_capable) entry.verified = VERIFIED[dv.vendor] || 'wired — not yet live-verified';

← 050399a all.dw: register Pierre Frey Fabrics microsite (pierrefreyfa  ·  back to All Designerwallcoverings  ·  Koroseal live-proven: reject synthesized catalog URLs (false 27f01e1 →