← 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
M data/live-stock-coverage.jsonM scripts/adapters/resolve.jsM scripts/adapters/schema-public.jsM scripts/build-coverage.js
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 →