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