[object Object]

← back to All Designerwallcoverings

live-stock: tune schema-public for Designers Guild + Marburg, remap Osborne SKUs, AS Creation honest-dim (8→11 live vendors)

0ccd9b9c0f238d0b58810c285aac62dabebfcd1b · 2026-07-07 12:46:24 -0700 · Steve

Files touched

Diff

commit 0ccd9b9c0f238d0b58810c285aac62dabebfcd1b
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 12:46:24 2026 -0700

    live-stock: tune schema-public for Designers Guild + Marburg, remap Osborne SKUs, AS Creation honest-dim (8→11 live vendors)
---
 data/live-stock-coverage.json     | 33 ++++++++++++++++++++-------------
 scripts/adapters/resolve.js       | 15 ++++++++++++++-
 scripts/adapters/schema-public.js | 37 ++++++++++++++++++++++++++++++++++---
 scripts/build-coverage.js         | 26 +++++++++++++++++++-------
 4 files changed, 87 insertions(+), 24 deletions(-)

diff --git a/data/live-stock-coverage.json b/data/live-stock-coverage.json
index 7c4ccf8..4ee93bc 100644
--- a/data/live-stock-coverage.json
+++ b/data/live-stock-coverage.json
@@ -1,5 +1,5 @@
 {
-  "generated_at": "2026-07-07T17:20:44.460Z",
+  "generated_at": "2026-07-07T19:46:15.280Z",
   "generator": "scripts/build-coverage.js",
   "total_display_vendors": 254,
   "counts": {
@@ -8,7 +8,7 @@
     "PORTAL_NO_CREDS": 18,
     "QUOTE_ONLY": 35,
     "NO_STOCK": 172,
-    "live_capable": 8
+    "live_capable": 11
   },
   "vendors": {
     "1838 Wallcoverings": {
@@ -194,7 +194,7 @@
       "tier": "PUBLIC_STOCK",
       "live_capable": false,
       "adapter": "schema-public",
-      "reason": "schema-public adapter wired but sampled SKUs return no definitive stock (JS-rendered availability — parse tuning pending)",
+      "reason": "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",
       "catalog_table": "as_creation_catalog",
       "backing": "AS Creation",
       "match": "exact",
@@ -614,14 +614,17 @@
     },
     "Designers Guild": {
       "tier": "PUBLIC_STOCK",
-      "live_capable": false,
+      "live_capable": true,
       "adapter": "schema-public",
-      "reason": "schema-public adapter wired but sampled SKUs return no definitive stock (JS-rendered availability — parse tuning pending)",
+      "reason": "public schema.org/Magento availability (no login required)",
       "catalog_table": "designers_guild_catalog",
       "backing": "Designers Guild",
       "match": "exact",
       "products": 879,
-      "active": 853
+      "active": 853,
+      "resolvable_active": 189,
+      "resolvable_pct": 22,
+      "verified": "wired — not yet live-verified"
     },
     "Designtex": {
       "tier": "PUBLIC_STOCK",
@@ -1645,14 +1648,17 @@
     },
     "Marburg": {
       "tier": "PUBLIC_STOCK",
-      "live_capable": false,
+      "live_capable": true,
       "adapter": "schema-public",
-      "reason": "schema-public adapter wired but sampled SKUs return no definitive stock (JS-rendered availability — parse tuning pending)",
+      "reason": "public schema.org/Magento availability (no login required)",
       "catalog_table": "marburg_catalog",
       "backing": "Marburg",
       "match": "exact",
       "products": 121,
-      "active": 112
+      "active": 112,
+      "resolvable_active": 96,
+      "resolvable_pct": 86,
+      "verified": "wired — not yet live-verified"
     },
     "Marimekko Exclusive": {
       "tier": "NO_STOCK",
@@ -1907,16 +1913,17 @@
     },
     "Osborne & Little": {
       "tier": "PUBLIC_STOCK",
-      "live_capable": false,
+      "live_capable": true,
       "adapter": "schema-public",
-      "reason": "adapter wired (schema-public) but display SKUs don't align with the osborne_catalog catalog (49 of 957 active resolvable) — needs SKU remap",
+      "reason": "public schema.org/Magento availability (no login required)",
       "catalog_table": "osborne_catalog",
       "backing": "Osborne & Little",
       "match": "exact",
       "products": 1041,
       "active": 957,
-      "resolvable_active": 49,
-      "resolvable_pct": 5
+      "resolvable_active": 350,
+      "resolvable_pct": 37,
+      "verified": "wired — not yet live-verified"
     },
     "Osborne & Little Fabrics": {
       "tier": "PUBLIC_STOCK",
diff --git a/scripts/adapters/resolve.js b/scripts/adapters/resolve.js
index 604848d..c2df8fa 100644
--- a/scripts/adapters/resolve.js
+++ b/scripts/adapters/resolve.js
@@ -23,7 +23,20 @@ async function resolveCatalogUrl(pool, table, gridSku) {
       `SELECT dw_sku, 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`, [gridSku]);
-    if (rows[0]) { if (rows[0].dw_sku) keys.add(String(rows[0].dw_sku).toUpperCase()); if (rows[0].mfr_sku) keys.add(String(rows[0].mfr_sku).toUpperCase()); }
+    if (rows[0]) {
+      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();
+        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
+        // keys on the bare code ("W7813-02"). Extract the leading <letters><digits>-<digits> code as
+        // an additional candidate key. It equals the whole string for already-clean SKUs (a no-op),
+        // so this only ADDS matches — it never breaks a vendor whose SKUs already align.
+        const lead = raw.match(/^[A-Z]+[0-9]+-[0-9]+/);
+        if (lead && lead[0] !== raw) keys.add(lead[0]);
+      }
+    }
   } catch { /* shopify_products lookup optional */ }
 
   const keyArr = [...keys];
diff --git a/scripts/adapters/schema-public.js b/scripts/adapters/schema-public.js
index 88c06e0..f8aa19e 100644
--- a/scripts/adapters/schema-public.js
+++ b/scripts/adapters/schema-public.js
@@ -45,7 +45,10 @@ async function run({ sku, pool, newSession, catalogTable, log }) {
     let domTxt = '';
     if (in_stock === null) {
       domTxt = await page.$$eval(
-        '[itemprop=availability], link[itemprop=availability], meta[itemprop=availability], .product [class*=stock], .product-info [class*=stock], [class*=availab], .stock, .product-form__inventory',
+        // Magento renders its stock state as <div class="stock_status" data-bind="text: stockStatus">
+        // (Knockout — empty in raw HTML, filled after JS; our 5s settle lets it populate), so
+        // .stock_status / [class*=stock_status] are added to the read set (catches Osborne & Little).
+        '[itemprop=availability], link[itemprop=availability], meta[itemprop=availability], .product [class*=stock], .product-info [class*=stock], .stock_status, [class*=stock_status], [class*=stock-status], [class*=availab], .stock, .product-form__inventory',
         (els) => els.map((e) => (e.getAttribute('href') || e.getAttribute('content') || e.getAttribute('title') || e.textContent || '').trim()).filter(Boolean).join(' | ')
       ).catch(() => '');
       const NAV = /(discontinued|sale|stock)\s+list|check\s+stock|stock\s+list/ig;
@@ -56,6 +59,34 @@ async function run({ sku, pool, newSession, catalogTable, log }) {
       else if (/in\s*stock|in-?stock|available/i.test(t)) in_stock = true;
     }
 
+    // 2.5 whole-document availability VOTE — some storefronts embed the true stock state in places
+    //     steps 1–2 don't reach: WooCommerce stamps it as a body class on each product tile
+    //     (<li class="product … instock|outofstock">, e.g. Marburg), and some Magento/CMS sites emit
+    //     it only as deeply escaped JSON inside a script string (e.g. Designers Guild's
+    //     \"itemAvailability\":\"InStock\"). We scan the rendered document for BOTH the schema
+    //     availability tokens and the Woo stock classes, then vote: any out-of-stock signal with NO
+    //     in-stock signal ⇒ false; any in-stock signal ⇒ true. Shape-general, not per-vendor.
+    let voteLabel = null;
+    if (in_stock === null) {
+      const vote = await page.evaluate(() => {
+        const html = document.documentElement.innerHTML || '';
+        const nRe = (re) => (html.match(re) || []).length;
+        // structural WooCommerce stock classes (whitespace-exact class token)
+        const wooIn = document.querySelectorAll('[class~="instock"]').length;
+        const wooOut = document.querySelectorAll('[class~="outofstock"]').length;
+        // schema.org availability tokens anywhere in the server-rendered markup (case-insensitive
+        // also catches the lowercase Woo class text, harmless double-count in the same direction)
+        const inTok = nRe(/InStock|LimitedAvailability|PreOrder/gi) + wooIn;
+        const outTok = nRe(/OutOfStock|SoldOut|Discontinued|BackOrder/gi) + wooOut;
+        return { inTok, outTok };
+      }).catch(() => ({ inTok: 0, outTok: 0 }));
+      if (vote.inTok || vote.outTok) {
+        if (vote.outTok > 0 && vote.inTok === 0) in_stock = false;
+        else if (vote.inTok > 0) in_stock = true;
+        voteLabel = `${in_stock ? 'in stock' : 'out of stock'} (${vote.inTok} in / ${vote.outTok} out signals)`;
+      }
+    }
+
     // 3. add-to-cart CTA state (last resort — presence of an enabled add-to-cart ⇒ purchasable).
     if (in_stock === null) {
       const cta = await page.$$eval(
@@ -67,8 +98,8 @@ async function run({ sku, pool, newSession, catalogTable, log }) {
       else if (cta && !/enabled/.test(cta)) in_stock = false;
     }
 
-    const label = in_stock === true ? 'in stock' : in_stock === false ? 'out of stock'
-      : (ldAvail || (domTxt ? domTxt.slice(0, 60) : 'unknown'));
+    const label = voteLabel || (in_stock === true ? 'in stock' : in_stock === false ? 'out of stock'
+      : (ldAvail || (domTxt ? domTxt.slice(0, 60) : 'unknown')));
 
     return {
       available: true, session_opened: true, vendor: null,
diff --git a/scripts/build-coverage.js b/scripts/build-coverage.js
index fbdfe4c..0c5f9e8 100644
--- a/scripts/build-coverage.js
+++ b/scripts/build-coverage.js
@@ -75,11 +75,16 @@ const SCHEMA_PUBLIC = new Set([
   'as creation', 'contrado', 'rebel walls',
 ]);
 
-// schema-public vendors WIRED but kept DIM: a metered probe (2026-07-07) reached the anonymous PDP
-// yet returned no definitive stock state (availability is JS-rendered / non-standard), so enabling
-// would burn a session for "unknown". Adapter stays attached so enabling is a one-flag change once
-// the parse is tuned. Honest: reachable, but no readable public stock signal on the sampled SKUs.
-const SCHEMA_PARSE_PENDING = new Set(['as creation', 'designers guild', 'marburg']);
+// schema-public vendors WIRED but kept DIM with an HONEST, SPECIFIC reason. Marburg (WooCommerce
+// instock class) and Designers Guild (escaped-JSON itemAvailability) were tuned into the adapter's
+// step-2.5 vote 2026-07-07 and are now live, so they're OFF this list. What remains is a vendor whose
+// anonymous PDP genuinely publishes NO stock — kept dim rather than faking in_stock.
+//   AS Creation: products.as-creation.com is a pattern-CATALOG/where-to-buy reference, not a store —
+//   the schema.org Offer block is empty, there is no cart and no availability markup (sold via
+//   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'],
+]);
 
 // Portals that require a login AND publish only PRICE (a wholesale price sheet / SFTP feed), never
 // live inventory — enabling a metered scrape would burn a session to read a price we can't surface.
@@ -206,7 +211,7 @@ async function main() {
     if (SCHEMA_PUBLIC.has(v) && oneToOne && dedicated(tbl) && hasUrls(tbl)) {
       if (SCHEMA_PARSE_PENDING.has(v))
         return { tier: 'PUBLIC_STOCK', live_capable: false, adapter: 'schema-public',
-          reason: 'schema-public adapter wired but sampled SKUs return no definitive stock (JS-rendered availability — parse tuning pending)', catalog_table: tbl };
+          reason: SCHEMA_PARSE_PENDING.get(v), catalog_table: tbl };
       return { tier: 'PUBLIC_STOCK', live_capable: true, adapter: 'schema-public',
         reason: 'public schema.org/Magento availability (no login required)', catalog_table: tbl };
     }
@@ -240,10 +245,17 @@ async function main() {
     if (!tbl || !/^[a-z0-9_]+$/.test(tbl) || tbl === 'vendor_catalog') return { active: 0, resolvable: 0 };
     try {
       const { rows } = await pool.query(
+        // The third OR mirrors resolve.js: match on the leading <letters><digits>-<digits> code
+        // extracted from a COMPOUND display mfr_sku (Osborne & Little "W7813-02-Villa Como-Aqua" →
+        // "W7813-02"). It's a pure ADD (an extra OR), so it can only raise a vendor's resolvable
+        // count — never drop one already passing — and it's a no-op for already-clean SKUs.
         `SELECT count(*) FILTER (WHERE upper(s.status)='ACTIVE')::int AS active,
                 count(*) FILTER (WHERE upper(s.status)='ACTIVE' AND EXISTS (
                    SELECT 1 FROM "${tbl}" c
-                    WHERE (upper(c.dw_sku)=upper(s.dw_sku) OR upper(coalesce(c.mfr_sku,''))=upper(coalesce(s.mfr_sku,'')))
+                    WHERE (upper(c.dw_sku)=upper(s.dw_sku)
+                        OR upper(coalesce(c.mfr_sku,''))=upper(coalesce(s.mfr_sku,''))
+                        OR (substring(s.mfr_sku from '^[A-Za-z]+[0-9]+-[0-9]+') IS NOT NULL
+                            AND upper(c.mfr_sku)=upper(substring(s.mfr_sku from '^[A-Za-z]+[0-9]+-[0-9]+'))))
                       AND c.product_url IS NOT NULL AND c.product_url <> ''))::int AS resolvable
            FROM shopify_products s WHERE s.vendor = $1`, [vendor]);
       return rows[0];

← 3790ba8 live-stock verify: 4 NEW vendors live-verified via schema-pu  ·  back to All Designerwallcoverings  ·  live-stock: structured-only availability vote (kills Osborne 77f866b →