← back to All Designerwallcoverings
live-stock: generic schema-public adapter (login-free schema.org/Magento availability) + reclassify 7 mis-tiered PORTAL_NO_CREDS vendors whose anonymous PDPs expose stock — AS Creation, Designers Guild, Graham & Brown, Innovations USA, Marburg, Mind the Gap, Rebel Walls now live-capable (resolvability-gated); Kravet honestly dimmed 'portal does not expose live stock (price sheet only)'; live-capable 4→11
d1be0d2988340b79366760d006d494ed1233df46 · 2026-07-07 10:15:28 -0700 · Steve
Files touched
M data/live-stock-coverage.jsonM scripts/adapters/index.jsA scripts/adapters/schema-public.jsM scripts/build-coverage.js
Diff
commit d1be0d2988340b79366760d006d494ed1233df46
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 10:15:28 2026 -0700
live-stock: generic schema-public adapter (login-free schema.org/Magento availability) + reclassify 7 mis-tiered PORTAL_NO_CREDS vendors whose anonymous PDPs expose stock — AS Creation, Designers Guild, Graham & Brown, Innovations USA, Marburg, Mind the Gap, Rebel Walls now live-capable (resolvability-gated); Kravet honestly dimmed 'portal does not expose live stock (price sheet only)'; live-capable 4→11
---
data/live-stock-coverage.json | 151 +++++++++++++++++++++++---------------
scripts/adapters/index.js | 1 +
scripts/adapters/schema-public.js | 81 ++++++++++++++++++++
scripts/build-coverage.js | 28 +++++++
4 files changed, 201 insertions(+), 60 deletions(-)
diff --git a/data/live-stock-coverage.json b/data/live-stock-coverage.json
index 62dabd7..63e15a3 100644
--- a/data/live-stock-coverage.json
+++ b/data/live-stock-coverage.json
@@ -1,14 +1,14 @@
{
- "generated_at": "2026-07-07T16:37:56.295Z",
+ "generated_at": "2026-07-07T17:15:01.445Z",
"generator": "scripts/build-coverage.js",
"total_display_vendors": 254,
"counts": {
"PORTAL_CREDS": 5,
- "PUBLIC_STOCK": 12,
- "PORTAL_NO_CREDS": 30,
+ "PUBLIC_STOCK": 24,
+ "PORTAL_NO_CREDS": 18,
"QUOTE_ONLY": 35,
"NO_STOCK": 172,
- "live_capable": 4
+ "live_capable": 11
},
"vendors": {
"1838 Wallcoverings": {
@@ -191,15 +191,18 @@
"active": 144
},
"AS Creation": {
- "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": "schema-public",
+ "reason": "public schema.org/Magento availability (no login required)",
"catalog_table": "as_creation_catalog",
"backing": "AS Creation",
"match": "exact",
"products": 505,
- "active": 327
+ "active": 327,
+ "resolvable_active": 326,
+ "resolvable_pct": 100,
+ "verified": "wired — not yet live-verified"
},
"Atomic 50 Ceilings": {
"tier": "NO_STOCK",
@@ -512,15 +515,17 @@
"active": 0
},
"Contrado": {
- "tier": "PORTAL_NO_CREDS",
+ "tier": "PUBLIC_STOCK",
"live_capable": false,
- "adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "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",
"catalog_table": "contrado_catalog",
"backing": "Contrado",
"match": "exact",
"products": 19,
- "active": 15
+ "active": 15,
+ "resolvable_active": 0,
+ "resolvable_pct": 0
},
"Coordonn��": {
"tier": "NO_STOCK",
@@ -611,26 +616,31 @@
"active": 0
},
"Designers Guild": {
- "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": "schema-public",
+ "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": "PORTAL_NO_CREDS",
+ "tier": "PUBLIC_STOCK",
"live_capable": false,
- "adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "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",
"catalog_table": "designtex_catalog",
"backing": "Designtex",
"match": "exact",
"products": 317,
- "active": 288
+ "active": 288,
+ "resolvable_active": 0,
+ "resolvable_pct": 0
},
"Dolce & Gabbana": {
"tier": "QUOTE_ONLY",
@@ -1073,15 +1083,18 @@
"active": 35
},
"Graham & Brown": {
- "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": "schema-public",
+ "reason": "public schema.org/Magento availability (no login required)",
"catalog_table": "graham_brown_catalog",
"backing": "Graham & Brown",
"match": "exact",
"products": 1679,
- "active": 405
+ "active": 405,
+ "resolvable_active": 399,
+ "resolvable_pct": 99,
+ "verified": "wired — not yet live-verified"
},
"Grasscloth": {
"tier": "QUOTE_ONLY",
@@ -1284,26 +1297,31 @@
"active": 0
},
"Innovations": {
- "tier": "PORTAL_NO_CREDS",
+ "tier": "PUBLIC_STOCK",
"live_capable": false,
- "adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "adapter": "schema-public",
+ "reason": "adapter wired (schema-public) but display SKUs don't align with the innovations_catalog catalog (0 of 8 active resolvable) — needs SKU remap",
"catalog_table": "innovations_catalog",
"backing": "Innovations",
"match": "exact",
"products": 9,
- "active": 8
+ "active": 8,
+ "resolvable_active": 0,
+ "resolvable_pct": 0
},
"Innovations USA": {
- "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": "schema-public",
+ "reason": "public schema.org/Magento availability (no login required)",
"catalog_table": "innovations_catalog",
"backing": "Innovations",
"match": "suffix-strip",
"products": 471,
- "active": 210
+ "active": 210,
+ "resolvable_active": 91,
+ "resolvable_pct": 43,
+ "verified": "wired — not yet live-verified"
},
"Jane Churchill": {
"tier": "NO_STOCK",
@@ -1386,7 +1404,7 @@
"tier": "PORTAL_NO_CREDS",
"live_capable": false,
"adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "reason": "vendor portal does not expose live stock (wholesale price sheet / SFTP only)",
"catalog_table": "kravet_catalog",
"backing": "Kravet",
"match": "exact",
@@ -1632,15 +1650,18 @@
"active": 0
},
"Marburg": {
- "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": "schema-public",
+ "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",
@@ -1720,15 +1741,18 @@
"active": 1
},
"Mind the Gap": {
- "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": "schema-public",
+ "reason": "public schema.org/Magento availability (no login required)",
"catalog_table": "mindthegap_catalog",
"backing": "Mind the Gap",
"match": "exact",
"products": 1261,
- "active": 1134
+ "active": 1134,
+ "resolvable_active": 924,
+ "resolvable_pct": 81,
+ "verified": "wired — not yet live-verified"
},
"Missoni Fabric": {
"tier": "QUOTE_ONLY",
@@ -1891,26 +1915,30 @@
"active": 1
},
"Osborne & Little": {
- "tier": "PORTAL_NO_CREDS",
+ "tier": "PUBLIC_STOCK",
"live_capable": false,
- "adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "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",
"catalog_table": "osborne_catalog",
"backing": "Osborne & Little",
"match": "exact",
"products": 1041,
- "active": 957
+ "active": 957,
+ "resolvable_active": 49,
+ "resolvable_pct": 5
},
"Osborne & Little Fabrics": {
- "tier": "PORTAL_NO_CREDS",
+ "tier": "PUBLIC_STOCK",
"live_capable": false,
- "adapter": null,
- "reason": "vendor portal exists but no trade credentials on file — supply creds to enable",
+ "adapter": "schema-public",
+ "reason": "adapter wired (schema-public) but display SKUs don't align with the osborne_catalog catalog (0 of 0 active resolvable) — needs SKU remap",
"catalog_table": "osborne_catalog",
"backing": "Osborne & Little",
"match": "suffix-strip",
"products": 1101,
- "active": 0
+ "active": 0,
+ "resolvable_active": 0,
+ "resolvable_pct": 0
},
"Parkertex": {
"tier": "NO_STOCK",
@@ -2201,15 +2229,18 @@
"active": 1
},
"Rebel Walls": {
- "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": "schema-public",
+ "reason": "public schema.org/Magento availability (no login required)",
"catalog_table": "rebelwalls_catalog",
"backing": "Rebel Walls",
"match": "exact",
"products": 3721,
- "active": 3613
+ "active": 3613,
+ "resolvable_active": 1028,
+ "resolvable_pct": 28,
+ "verified": "wired — not yet live-verified"
},
"Retro Walls": {
"tier": "NO_STOCK",
diff --git a/scripts/adapters/index.js b/scripts/adapters/index.js
index e5ab39e..d637c0d 100644
--- a/scripts/adapters/index.js
+++ b/scripts/adapters/index.js
@@ -16,6 +16,7 @@ const ADAPTERS = {
romo: require('./romo'),
'shopify-public': require('./shopify-public'),
'woo-public': require('./woo-public'),
+ 'schema-public': require('./schema-public'), // login-free schema.org/Magento availability
};
function get(key) { return ADAPTERS[key] || null; }
diff --git a/scripts/adapters/schema-public.js b/scripts/adapters/schema-public.js
new file mode 100644
index 0000000..323aea8
--- /dev/null
+++ b/scripts/adapters/schema-public.js
@@ -0,0 +1,81 @@
+// adapters/schema-public.js — PUBLIC-STOCK adapter for vendors whose product pages render
+// availability WITHOUT a login. Many "to-the-trade" storefronts (Magento, custom CMS) still
+// emit schema.org Product/Offer JSON-LD (offers.availability = InStock/OutOfStock) and/or a
+// visible stock indicator + add-to-cart state on the ANONYMOUS product page. This adapter reads
+// that public signal — it never logs in, so no trade credentials are required for these vendors.
+//
+// Shape-general (NOT per-vendor special-cased): it resolves the grid SKU → the catalog product_url
+// (bridged through shopify_products), opens the SAME Browserbase session mechanism the portal
+// adapters use, and derives availability from, in priority order:
+// 1. schema.org JSON-LD offers.availability (authoritative when present)
+// 2. product-scoped DOM stock markup ([itemprop=availability], .stock, [class*=availab])
+// 3. add-to-cart button presence/enabled state (a disabled/absent CTA ⇒ out of stock)
+//
+// PUBLIC-SAFE: emits availability only. price is ALWAYS null — a vendor's public retail is NOT our
+// retail, so it's never surfaced (same contract as shopify-public / woo-public).
+const { resolveCatalogUrl } = require('./resolve');
+
+async function run({ sku, pool, newSession, catalogTable, log }) {
+ let sessionOpened = false, browser = null, session = null, bb = null;
+ try {
+ const r = await resolveCatalogUrl(pool, catalogTable, sku);
+ if (r.error) return { available: false, reason: r.error };
+ const url = r.product_url;
+
+ const sess = await newSession();
+ ({ browser, session, bb } = sess); const page = sess.page;
+ sessionOpened = true;
+ log('bb session', session && session.id, '→', url);
+
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.waitForTimeout(5000); // let any client-rendered availability settle
+
+ // 1. schema.org JSON-LD — parse every ld+json block, look for an Offer availability.
+ const ldRaw = await page.$$eval('script[type="application/ld+json"]', (els) => els.map((e) => e.textContent).join('\n')).catch(() => '');
+ let in_stock = null, ldAvail = null;
+ if (ldRaw) {
+ const m = ldRaw.match(/"availability"\s*:\s*"([^"]+)"/i);
+ if (m) { ldAvail = m[1]; if (/OutOfStock|SoldOut|Discontinued|BackOrder/i.test(ldAvail)) in_stock = false; else if (/InStock|LimitedAvailability|PreOrder|OnlineOnly|InStoreOnly/i.test(ldAvail)) in_stock = true; }
+ }
+
+ // 2. product-scoped DOM stock markup (avoid global nav chrome).
+ let domTxt = '';
+ if (in_stock === null) {
+ domTxt = await page.$$eval(
+ '[itemprop=availability], .product [class*=stock], .product-info [class*=stock], [class*=availab], .stock, .product-form__inventory',
+ (els) => els.map((e) => (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;
+ const t = String(domTxt).replace(NAV, '');
+ if (/out\s*of\s*stock|sold\s*out|unavailable|no longer available/i.test(t)) in_stock = false;
+ else if (/in\s*stock|in-?stock|available/i.test(t)) in_stock = true;
+ }
+
+ // 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(
+ 'button[name=add], button[type=submit], [class*=add-to], [class*=addto], a[href*=cart]',
+ (els) => els.filter((e) => /add to (cart|basket|bag|sample)|add sample/i.test(e.textContent || e.getAttribute('aria-label') || ''))
+ .map((e) => (e.disabled || /disabled/i.test(e.className) ? 'disabled' : 'enabled')).join(',')
+ ).catch(() => '');
+ if (/enabled/.test(cta)) in_stock = true;
+ 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'));
+
+ return {
+ available: true, session_opened: true, vendor: null,
+ in_stock, lead_time: null, price: null, // public retail is NOT our retail → never emitted
+ raw_stock_label: label, as_of: new Date().toISOString(), source: 'schema-public-live',
+ };
+ } catch (e) {
+ return { available: false, session_opened: sessionOpened, reason: 'live scrape error: ' + (e.message || e) };
+ } finally {
+ try { if (browser) await browser.close(); } catch {}
+ try { if (bb && session) await bb.sessions.update(session.id, { projectId: session.projectId, status: 'REQUEST_RELEASE' }); } catch {}
+ }
+}
+
+module.exports = { run };
diff --git a/scripts/build-coverage.js b/scripts/build-coverage.js
index d7328c3..1b8f00e 100644
--- a/scripts/build-coverage.js
+++ b/scripts/build-coverage.js
@@ -59,6 +59,25 @@ const VERIFIED = {
'Romo': 'reaches live portal 2026-07-07 ($0.08) — stock-parse selectors pending tuning',
};
+// PUBLIC-SCHEMA vendors — to-the-trade / Magento / custom storefronts whose ANONYMOUS product
+// pages emit schema.org Offer availability (or visible Magento stock markup), so live stock is
+// readable WITHOUT a trade login. Probed 2026-07-07 (free HTTP HEAD/GET, no session). These were
+// mis-tiered as PORTAL_NO_CREDS purely because a login_url exists — but a login_url gates checkout,
+// not the public availability signal. Routed to the login-free `schema-public` adapter; still
+// subject to the resolvability gate (display SKUs must align with the catalog product_urls).
+const SCHEMA_PUBLIC = new Set([
+ 'mind the gap', 'osborne & little', 'osborne & little fabrics', 'designers guild',
+ 'innovations', 'innovations usa', 'marburg', 'designtex', 'graham & brown',
+ 'as creation', 'contrado', 'rebel walls',
+]);
+
+// 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.
+// Honest dim: creds may exist, but the portal has no live-stock signal to read.
+const NO_LIVE_STOCK_PORTAL = new Set([
+ 'kravet', // Kravet SFTP master = MAP/wholesale price sheet only; no per-SKU live inventory
+]);
+
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',
@@ -172,6 +191,15 @@ async function main() {
if (oneToOne && dedicated(tbl) && hasUrls(tbl) && catShopify.get(tbl))
return { tier: 'PUBLIC_STOCK', live_capable: true, adapter: 'shopify-public',
reason: 'public Shopify storefront stock (.js feed)', catalog_table: tbl };
+ // 2b. PUBLIC_STOCK (schema.org) — probed vendors whose anonymous PDP emits Offer availability.
+ // Precedes the portal branches so a login_url doesn't force these to dim as PORTAL_NO_CREDS.
+ if (SCHEMA_PUBLIC.has(v) && oneToOne && dedicated(tbl) && hasUrls(tbl))
+ return { tier: 'PUBLIC_STOCK', live_capable: true, adapter: 'schema-public',
+ reason: 'public schema.org/Magento availability (no login required)', catalog_table: tbl };
+ // 2c. PORTAL, PRICE-ONLY — login publishes a price sheet, not live stock → honest dim.
+ if (NO_LIVE_STOCK_PORTAL.has(v))
+ return { tier: 'PORTAL_NO_CREDS', live_capable: false, adapter: null,
+ reason: 'vendor portal does not expose live stock (wholesale price sheet / SFTP only)', catalog_table: tbl };
if (backing.has_pass) // creds exist but no adapter wired for this specific portal yet
return { tier: 'PORTAL_CREDS', live_capable: false, adapter: null,
reason: 'backing portal has trade creds but no live adapter wired yet', catalog_table: tbl };
← 2c397b4 live-stock: daily $ kill-switch — global per-day spend ceili
·
back to All Designerwallcoverings
·
live-stock verify: 4 NEW vendors live-verified via schema-pu 3790ba8 →