← back to Dw Validator Debug TK11314
TK-11047 add verified Carnegie spec bulk runner
23131e832dd60bf02c7713f3d6fb353dd077d929 · 2026-08-31 13:52:16 -0700 · Steve
Files touched
A data/carnegie-spec-E2E-2026-08-31T20-49-30-681Z.jsonA data/carnegie-spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.jsonA data/carnegie-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.jsonA scripts/carnegie-spec-bulk.mjsA scripts/verify-carnegie-spec-result.mjs
Diff
commit 23131e832dd60bf02c7713f3d6fb353dd077d929
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 31 13:52:16 2026 -0700
TK-11047 add verified Carnegie spec bulk runner
---
...carnegie-spec-E2E-2026-08-31T20-49-30-681Z.json | 70 ++++++++
...spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.json | 59 +++++++
...-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.json | 193 +++++++++++++++++++++
scripts/carnegie-spec-bulk.mjs | 137 +++++++++++++++
scripts/verify-carnegie-spec-result.mjs | 30 ++++
5 files changed, 489 insertions(+)
diff --git a/data/carnegie-spec-E2E-2026-08-31T20-49-30-681Z.json b/data/carnegie-spec-E2E-2026-08-31T20-49-30-681Z.json
new file mode 100644
index 00000000..dab350b9
--- /dev/null
+++ b/data/carnegie-spec-E2E-2026-08-31T20-49-30-681Z.json
@@ -0,0 +1,70 @@
+{
+ "ticket": "TK-11047",
+ "risk_tier": "R4",
+ "timestamp": "2026-08-31T20:51:41.033Z",
+ "result_path": "data/carnegie-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.json",
+ "canonical_count": 5,
+ "negative_auth_status": 401,
+ "checks": [
+ {
+ "shopify_id": "gid://shopify/Product/7923134693427",
+ "canonical_present": true,
+ "canonical_mfr_match": true,
+ "canonical_body_nonempty": true,
+ "canonical_meta_present": true,
+ "shopify_status": 200,
+ "shopify_body_nonempty": true,
+ "shopify_source_present": true,
+ "shopify_meta_status": 200,
+ "shopify_meta_mfr_match": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134791731",
+ "canonical_present": true,
+ "canonical_mfr_match": true,
+ "canonical_body_nonempty": true,
+ "canonical_meta_present": true,
+ "shopify_status": 200,
+ "shopify_body_nonempty": true,
+ "shopify_source_present": true,
+ "shopify_meta_status": 200,
+ "shopify_meta_mfr_match": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134857267",
+ "canonical_present": true,
+ "canonical_mfr_match": true,
+ "canonical_body_nonempty": true,
+ "canonical_meta_present": true,
+ "shopify_status": 200,
+ "shopify_body_nonempty": true,
+ "shopify_source_present": true,
+ "shopify_meta_status": 200,
+ "shopify_meta_mfr_match": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134955571",
+ "canonical_present": true,
+ "canonical_mfr_match": true,
+ "canonical_body_nonempty": true,
+ "canonical_meta_present": true,
+ "shopify_status": 200,
+ "shopify_body_nonempty": true,
+ "shopify_source_present": true,
+ "shopify_meta_status": 200,
+ "shopify_meta_mfr_match": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923135021107",
+ "canonical_present": true,
+ "canonical_mfr_match": true,
+ "canonical_body_nonempty": true,
+ "canonical_meta_present": true,
+ "shopify_status": 200,
+ "shopify_body_nonempty": true,
+ "shopify_source_present": true,
+ "shopify_meta_status": 200,
+ "shopify_meta_mfr_match": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/carnegie-spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.json b/data/carnegie-spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.json
new file mode 100644
index 00000000..9bb42091
--- /dev/null
+++ b/data/carnegie-spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.json
@@ -0,0 +1,59 @@
+{
+ "ticket": "TK-11047",
+ "timestamp": "2026-08-31T20:49:32.914Z",
+ "domain": "designer-laboratory-sandbox.myshopify.com",
+ "offset": 0,
+ "limit": 5,
+ "products": [
+ {
+ "shopify_id": "gid://shopify/Product/7923134693427",
+ "product": {
+ "id": 7923134693427,
+ "title": "Carnegie Siltech Grain 1",
+ "body_html": "<p>A medium grained leather look in 100% silicone: a cleanable, durable, sustainable and PVC-free indoor/outdoor alternative to vinyl. Siltech Plus coated upholsteries are 2nd generation silicone technology using 3 layers of silicone for increased strength and an improved natural hand. Siltech Plus products deliver unmatched scratch and tear resistance along with incredible stain resistance and ease of cleaning all with no added finishes.</p>",
+ "tags": "63001, Alabaster, Carnegie, display_variant, Leather-Look, Light, Siltech Grain, split-batch:TK-10686, White",
+ "status": "active"
+ }
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134791731",
+ "product": {
+ "id": 7923134791731,
+ "title": "Carnegie Siltech Grain 2",
+ "body_html": "<p>A medium grained leather look in 100% silicone: a cleanable, durable, sustainable and PVC-free indoor/outdoor alternative to vinyl. Siltech Plus coated upholsteries are 2nd generation silicone technology using 3 layers of silicone for increased strength and an improved natural hand. Siltech Plus products deliver unmatched scratch and tear resistance along with incredible stain resistance and ease of cleaning all with no added finishes.</p>",
+ "tags": "63002, Carnegie, display_variant, Leather-Look, Oatmeal, Siltech Grain, split-batch:TK-10686, Warm",
+ "status": "active"
+ }
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134857267",
+ "product": {
+ "id": 7923134857267,
+ "title": "Carnegie Siltech Grain 3",
+ "body_html": "<p>A medium grained leather look in 100% silicone: a cleanable, durable, sustainable and PVC-free indoor/outdoor alternative to vinyl. Siltech Plus coated upholsteries are 2nd generation silicone technology using 3 layers of silicone for increased strength and an improved natural hand. Siltech Plus products deliver unmatched scratch and tear resistance along with incredible stain resistance and ease of cleaning all with no added finishes.</p>",
+ "tags": "63003, Carnegie, display_variant, Leather-Look, Neutral, Siltech Grain, split-batch:TK-10686, Taupe",
+ "status": "active"
+ }
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134955571",
+ "product": {
+ "id": 7923134955571,
+ "title": "Carnegie Siltech Grain 4",
+ "body_html": "<p>A medium grained leather look in 100% silicone: a cleanable, durable, sustainable and PVC-free indoor/outdoor alternative to vinyl. Siltech Plus coated upholsteries are 2nd generation silicone technology using 3 layers of silicone for increased strength and an improved natural hand. Siltech Plus products deliver unmatched scratch and tear resistance along with incredible stain resistance and ease of cleaning all with no added finishes.</p>",
+ "tags": "63004, Carnegie, display_variant, Leather-Look, Loden, Moss, Neutral, Siltech Grain, split-batch:TK-10686",
+ "status": "active"
+ }
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923135021107",
+ "product": {
+ "id": 7923135021107,
+ "title": "Carnegie Siltech Grain 5",
+ "body_html": "<p>A medium grained leather look in 100% silicone: a cleanable, durable, sustainable and PVC-free indoor/outdoor alternative to vinyl. Siltech Plus coated upholsteries are 2nd generation silicone technology using 3 layers of silicone for increased strength and an improved natural hand. Siltech Plus products deliver unmatched scratch and tear resistance along with incredible stain resistance and ease of cleaning all with no added finishes.</p>",
+ "tags": "63005, Carnegie, Dark, display_variant, Espresso, Leather-Look, Siltech Grain, split-batch:TK-10686",
+ "status": "active"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/carnegie-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.json b/data/carnegie-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.json
new file mode 100644
index 00000000..b3a72902
--- /dev/null
+++ b/data/carnegie-spec-bulk-RESULT-2026-08-31T20-49-30-681Z.json
@@ -0,0 +1,193 @@
+{
+ "ticket": "TK-11047",
+ "apply": true,
+ "total_products": 5921,
+ "catalog_rows": 5916,
+ "deterministic_mapped": 4604,
+ "held": 1317,
+ "held_reasons": {
+ "no_match": 1303,
+ "title_shape": 14
+ },
+ "held_sample": [
+ {
+ "shopify_id": "gid://shopify/Product/7923185713203",
+ "title": "Carnegie Casa 486",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185745971",
+ "title": "Carnegie Casa 395",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185811507",
+ "title": "Carnegie Casa 403",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185844275",
+ "title": "Carnegie Casa 411",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185877043",
+ "title": "Carnegie Casa 429",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185909811",
+ "title": "Carnegie Casa 437",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185942579",
+ "title": "Carnegie Casa 445",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923185975347",
+ "title": "Carnegie Casa 452",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923186040883",
+ "title": "Carnegie Casa 460",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923186073651",
+ "title": "Carnegie Casa 478",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923186106419",
+ "title": "Carnegie Casa 486",
+ "reason": "no_match"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923191578675",
+ "title": "Carnegie Umbria IV 101",
+ "reason": "no_match"
+ }
+ ],
+ "offset": 0,
+ "limit": 5,
+ "tranche": 5,
+ "tranche_sample": [
+ {
+ "shopify_id": "gid://shopify/Product/7923134693427",
+ "title": "Carnegie Siltech Grain 1",
+ "mfr_sku": "63001",
+ "pattern": "Siltech Grain",
+ "color": "1",
+ "product_type": "Upholstery",
+ "method": "mfr_sku"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134791731",
+ "title": "Carnegie Siltech Grain 2",
+ "mfr_sku": "63002",
+ "pattern": "Siltech Grain",
+ "color": "2",
+ "product_type": "Upholstery",
+ "method": "mfr_sku"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134857267",
+ "title": "Carnegie Siltech Grain 3",
+ "mfr_sku": "63003",
+ "pattern": "Siltech Grain",
+ "color": "3",
+ "product_type": "Upholstery",
+ "method": "mfr_sku"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134955571",
+ "title": "Carnegie Siltech Grain 4",
+ "mfr_sku": "63004",
+ "pattern": "Siltech Grain",
+ "color": "4",
+ "product_type": "Upholstery",
+ "method": "mfr_sku"
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923135021107",
+ "title": "Carnegie Siltech Grain 5",
+ "mfr_sku": "63005",
+ "pattern": "Siltech Grain",
+ "color": "5",
+ "product_type": "Upholstery",
+ "method": "mfr_sku"
+ }
+ ],
+ "methods": {
+ "mfr_sku": 5
+ },
+ "restore_path": "/Users/macstudio3/Projects/Designer-Wallcoverings/data/carnegie-spec-bulk-RESTORE-2026-08-31T20-49-30-681Z.json",
+ "written": [
+ {
+ "shopify_id": "gid://shopify/Product/7923134693427",
+ "mfr_sku": "63001",
+ "status": 200
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134791731",
+ "mfr_sku": "63002",
+ "status": 200
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134857267",
+ "mfr_sku": "63003",
+ "status": 200
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134955571",
+ "mfr_sku": "63004",
+ "status": 200
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923135021107",
+ "mfr_sku": "63005",
+ "status": 200
+ }
+ ],
+ "failed": [],
+ "verified": [
+ {
+ "shopify_id": "gid://shopify/Product/7923134693427",
+ "response_body_nonempty": true,
+ "source_present": true,
+ "missing_labels": [],
+ "pattern_tag_present": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134791731",
+ "response_body_nonempty": true,
+ "source_present": true,
+ "missing_labels": [],
+ "pattern_tag_present": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134857267",
+ "response_body_nonempty": true,
+ "source_present": true,
+ "missing_labels": [],
+ "pattern_tag_present": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923134955571",
+ "response_body_nonempty": true,
+ "source_present": true,
+ "missing_labels": [],
+ "pattern_tag_present": true
+ },
+ {
+ "shopify_id": "gid://shopify/Product/7923135021107",
+ "response_body_nonempty": true,
+ "source_present": true,
+ "missing_labels": [],
+ "pattern_tag_present": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/carnegie-spec-bulk.mjs b/scripts/carnegie-spec-bulk.mjs
new file mode 100644
index 00000000..ce2e26e3
--- /dev/null
+++ b/scripts/carnegie-spec-bulk.mjs
@@ -0,0 +1,137 @@
+#!/usr/bin/env node
+// TK-11047: deterministic Carnegie specs -> canonical dw_unified first -> Shopify.
+// Dry-run by default. Apply requires --apply and is capped at 500 rows per invocation.
+import fs from 'node:fs';
+import path from 'node:path';
+import { execFileSync } from 'node:child_process';
+
+const ROOT = '/Users/macstudio3/Projects/Designer-Wallcoverings';
+const args = new Map(process.argv.slice(2).map((v, i, a) => v.startsWith('--') ? [v, a[i + 1]?.startsWith('--') ? true : a[i + 1]] : [null, null]).filter(([k]) => k));
+const apply = args.has('--apply');
+const limit = Math.min(Number(args.get('--limit') || 100), 500);
+const offset = Number(args.get('--offset') || 0);
+if (!Number.isInteger(limit) || limit < 1 || !Number.isInteger(offset) || offset < 0) throw new Error('invalid --limit/--offset');
+
+const env = Object.fromEntries(fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n').filter(x => x.includes('=')).map(x => {
+ const i = x.indexOf('='); return [x.slice(0, i).trim(), x.slice(i + 1).trim()];
+}));
+const token = env.SHOPIFY_ADMIN_TOKEN || env.SHOPIFY_ADMIN_ACCESS_TOKEN;
+const domain = env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
+if (apply && !token) throw new Error('missing Shopify token');
+const API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const q = s => String(s ?? '').replace(/'/g, "''");
+const norm = s => String(s ?? '').trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
+const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
+const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+
+function psqlJson(command, sql) {
+ const out = execFileSync(command[0], [...command.slice(1), '-tA'], { input: sql, encoding: 'utf8', maxBuffer: 128 << 20 }).trim();
+ return out ? JSON.parse(out) : [];
+}
+
+const catalog = psqlJson(['psql', '-h', '/tmp', '-d', 'dw_unified'], `
+ SELECT coalesce(json_agg(x), '[]'::json) FROM (
+ SELECT id,mfr_sku,pattern_name,color_number,product_type,product_url,body_html,specs
+ FROM carnegie_catalog WHERE specs IS NOT NULL AND jsonb_typeof(specs)='object'
+ ) x;
+`);
+const products = psqlJson(['ssh', 'root@45.61.58.125', 'psql', 'dw_unified'], `
+ SELECT coalesce(json_agg(x), '[]'::json) FROM (
+ SELECT shopify_id,title,product_type,mfr_sku,body_html,metafields
+ FROM shopify_products WHERE status='ACTIVE' AND vendor='Carnegie'
+ ORDER BY shopify_id
+ ) x;
+`);
+
+const byMfr = new Map(catalog.map(c => [norm(c.mfr_sku), [c]]));
+const byTitle = new Map();
+for (const c of catalog) {
+ const key = [norm(c.pattern_name), norm(c.color_number), norm(c.product_type)].join('|');
+ if (!byTitle.has(key)) byTitle.set(key, []);
+ byTitle.get(key).push(c);
+}
+
+function titleParts(p) {
+ const m = String(p.title || '').match(/^Carnegie\s+(.+)\s+([A-Za-z0-9]+)$/);
+ return m ? [m[1], m[2], p.product_type] : null;
+}
+function resolve(p) {
+ if (p.mfr_sku && byMfr.get(norm(p.mfr_sku))?.length === 1) return { c: byMfr.get(norm(p.mfr_sku))[0], method: 'mfr_sku' };
+ const parts = titleParts(p); if (!parts) return { reason: 'title_shape' };
+ const candidates = byTitle.get(parts.map(norm).join('|')) || [];
+ return candidates.length === 1 ? { c: candidates[0], method: 'title_color_type' } : { reason: candidates.length ? 'ambiguous' : 'no_match' };
+}
+function enriched(c) {
+ const specs = c.specs || {};
+ const desc = String(c.body_html || '').replace(/<style[\s\S]*?<\/style>/gi, '').trim();
+ const lis = Object.entries(specs).filter(([, v]) => String(v ?? '').trim()).map(([k, v]) => `<li><strong>${esc(k)}:</strong> ${esc(v)}</li>`).join('');
+ const html = `${desc}<h3>Manufacturer Specifications</h3><ul>${lis}</ul><p><a href="${esc(c.product_url)}" rel="noopener" target="_blank">Manufacturer product page</a></p>`;
+ const meta = { manufacturer: 'Carnegie', manufacturer_sku: c.mfr_sku, source_url: c.product_url, specs };
+ return { html, meta };
+}
+
+const resolved = [], held = [];
+for (const p of products) {
+ const r = resolve(p);
+ if (!r.c) { held.push({ shopify_id: p.shopify_id, title: p.title, reason: r.reason }); continue; }
+ const e = enriched(r.c);
+ resolved.push({ ...p, catalog: r.c, method: r.method, ...e });
+}
+const tranche = resolved.slice(offset, offset + limit);
+const summary = { ticket: 'TK-11047', apply, total_products: products.length, catalog_rows: catalog.length, deterministic_mapped: resolved.length, held: held.length, held_reasons: held.reduce((a, x) => (a[x.reason] = (a[x.reason] || 0) + 1, a), {}), held_sample: held.slice(0, 12), offset, limit, tranche: tranche.length, tranche_sample: tranche.slice(0, 5).map(x => ({ shopify_id: x.shopify_id, title: x.title, mfr_sku: x.catalog.mfr_sku, pattern: x.catalog.pattern_name, color: x.catalog.color_number, product_type: x.catalog.product_type, method: x.method })), methods: tranche.reduce((a, x) => (a[x.method] = (a[x.method] || 0) + 1, a), {}) };
+console.log(JSON.stringify(summary, null, 2));
+if (!apply) process.exit(0);
+if (!tranche.length) process.exit(0);
+
+async function rest(method, endpoint, body) {
+ const res = await fetch(`https://${domain}/admin/api/${API}/${endpoint}`, { method, headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
+ const text = await res.text(); let json; try { json = JSON.parse(text); } catch { json = { raw: text }; }
+ return { status: res.status, json };
+}
+
+// Backup current Shopify state for the entire tranche before either boundary is mutated.
+const restore = { ticket: 'TK-11047', timestamp: new Date().toISOString(), domain, offset, limit, products: [] };
+for (const p of tranche) {
+ const id = p.shopify_id.replace('gid://shopify/Product/', '');
+ const g = await rest('GET', `products/${id}.json?fields=id,title,body_html,tags,status`);
+ if (g.status !== 200 || !g.json.product) throw new Error(`backup GET ${id} failed ${g.status}`);
+ restore.products.push({ shopify_id: p.shopify_id, product: g.json.product });
+ await sleep(520);
+}
+const restorePath = path.join(ROOT, `data/carnegie-spec-bulk-RESTORE-${stamp}.json`);
+fs.writeFileSync(restorePath, JSON.stringify(restore, null, 2));
+
+// Canonical backup + write is one transaction and happens before any Shopify update.
+const payload = tranche.map(p => ({ shopify_id: p.shopify_id, mfr_sku: p.catalog.mfr_sku, body_html: p.html, manufacturer_specs: p.meta, manufacturer_product_url: p.catalog.product_url }));
+const b64 = Buffer.from(JSON.stringify(payload)).toString('base64');
+execFileSync('ssh', ['root@45.61.58.125', 'psql', 'dw_unified'], { input: `
+BEGIN;
+CREATE TABLE IF NOT EXISTS carnegie_spec_restore_20260831 AS SELECT * FROM shopify_products WITH NO DATA;
+INSERT INTO carnegie_spec_restore_20260831 SELECT s.* FROM shopify_products s JOIN jsonb_to_recordset(convert_from(decode('${b64}','base64'),'UTF8')::jsonb) AS x(shopify_id text) ON x.shopify_id=s.shopify_id WHERE NOT EXISTS (SELECT 1 FROM carnegie_spec_restore_20260831 b WHERE b.shopify_id=s.shopify_id);
+WITH x AS (SELECT * FROM jsonb_to_recordset(convert_from(decode('${b64}','base64'),'UTF8')::jsonb) AS r(shopify_id text,mfr_sku text,body_html text,manufacturer_specs jsonb,manufacturer_product_url text))
+UPDATE shopify_products s SET mfr_sku=x.mfr_sku,body_html=x.body_html,metafields=coalesce(s.metafields,'{}'::jsonb)||jsonb_build_object('manufacturer_specs',x.manufacturer_specs,'manufacturer_product_url',x.manufacturer_product_url) FROM x WHERE s.shopify_id=x.shopify_id;
+COMMIT;
+`, stdio: ['pipe', 'inherit', 'inherit'] });
+
+const result = { ...summary, restore_path: restorePath, written: [], failed: [], verified: [] };
+for (let i = 0; i < tranche.length; i++) {
+ const p = tranche[i], id = p.shopify_id.replace('gid://shopify/Product/', '');
+ const old = restore.products[i].product;
+ const tags = new Set(String(old.tags || '').split(',').map(x => x.trim()).filter(Boolean));
+ if (p.catalog.pattern_name) tags.add(p.catalog.pattern_name);
+ const u = await rest('PUT', `products/${id}.json`, { product: { id: Number(id), body_html: p.html, tags: [...tags].join(', '), metafields: [{ namespace: 'custom', key: 'manufacturer_specs', type: 'json', value: JSON.stringify(p.meta) }] } });
+ if (u.status !== 200) result.failed.push({ shopify_id: p.shopify_id, status: u.status, error: u.json });
+ else {
+ const body = u.json.product?.body_html || '';
+ const labels = Object.keys(p.catalog.specs || {}).filter(k => String(p.catalog.specs[k] ?? '').trim());
+ result.written.push({ shopify_id: p.shopify_id, mfr_sku: p.catalog.mfr_sku, status: u.status });
+ result.verified.push({ shopify_id: p.shopify_id, response_body_nonempty: body.length > 20, source_present: body.includes(p.catalog.product_url), missing_labels: labels.filter(k => !body.includes(k)), pattern_tag_present: String(u.json.product?.tags || '').split(',').map(x => x.trim()).includes(p.catalog.pattern_name) });
+ }
+ await sleep(520);
+}
+const resultPath = path.join(ROOT, `data/carnegie-spec-bulk-RESULT-${stamp}.json`);
+fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
+const bad = result.failed.length || result.verified.some(v => !v.response_body_nonempty || !v.source_present || v.missing_labels.length || !v.pattern_tag_present);
+console.log(JSON.stringify({ result_path: resultPath, restore_path: restorePath, written: result.written.length, failed: result.failed.length, bad_verifications: result.verified.filter(v => !v.response_body_nonempty || !v.source_present || v.missing_labels.length || !v.pattern_tag_present).length }, null, 2));
+if (bad) process.exit(2);
diff --git a/scripts/verify-carnegie-spec-result.mjs b/scripts/verify-carnegie-spec-result.mjs
new file mode 100644
index 00000000..b781ad2f
--- /dev/null
+++ b/scripts/verify-carnegie-spec-result.mjs
@@ -0,0 +1,30 @@
+#!/usr/bin/env node
+// Independent R4 read-back verifier for TK-11047 result artifacts.
+import fs from 'node:fs';
+import path from 'node:path';
+import { execFileSync } from 'node:child_process';
+const ROOT = '/Users/macstudio3/Projects/Designer-Wallcoverings';
+const resultPath = process.argv[2];
+if (!resultPath) throw new Error('usage: verify-carnegie-spec-result.mjs <result.json>');
+const result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
+const env = Object.fromEntries(fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n').filter(x => x.includes('=')).map(x => { const i=x.indexOf('='); return [x.slice(0,i).trim(),x.slice(i+1).trim()]; }));
+const token=env.SHOPIFY_ADMIN_TOKEN||env.SHOPIFY_ADMIN_ACCESS_TOKEN, domain=env.SHOPIFY_STORE_DOMAIN||'designer-laboratory-sandbox.myshopify.com';
+if(!token) throw new Error('missing Shopify token');
+const ids=result.written.map(x=>x.shopify_id), b64=Buffer.from(JSON.stringify(ids)).toString('base64');
+const canonicalRaw=execFileSync('ssh',['root@45.61.58.125','psql','dw_unified','-tA'],{input:`SELECT coalesce(json_agg(x),'[]'::json) FROM (SELECT shopify_id,mfr_sku,length(body_html) body_length,metafields->'manufacturer_specs' manufacturer_specs,metafields->>'manufacturer_product_url' source_url FROM shopify_products WHERE shopify_id IN (SELECT jsonb_array_elements_text(convert_from(decode('${b64}','base64'),'UTF8')::jsonb))) x;`,encoding:'utf8',maxBuffer:32<<20}).trim();
+const canonical=JSON.parse(canonicalRaw||'[]'), byId=new Map(canonical.map(x=>[x.shopify_id,x]));
+const checks=[];
+for(const w of result.written){
+ const id=w.shopify_id.replace('gid://shopify/Product/','');
+ const headers={'X-Shopify-Access-Token':token,'Content-Type':'application/json'};
+ const pr=await fetch(`https://${domain}/admin/api/2024-10/products/${id}.json?fields=id,title,body_html,tags`,{headers}); const pj=await pr.json();
+ const mr=await fetch(`https://${domain}/admin/api/2024-10/products/${id}/metafields.json?namespace=custom&key=manufacturer_specs`,{headers}); const mj=await mr.json();
+ const c=byId.get(w.shopify_id), meta=mj.metafields?.find(x=>x.namespace==='custom'&&x.key==='manufacturer_specs');
+ let parsed=null; try{parsed=JSON.parse(meta?.value||'');}catch{}
+ checks.push({shopify_id:w.shopify_id,canonical_present:!!c,canonical_mfr_match:c?.mfr_sku===w.mfr_sku,canonical_body_nonempty:c?.body_length>20,canonical_meta_present:!!c?.manufacturer_specs,shopify_status:pr.status,shopify_body_nonempty:(pj.product?.body_html||'').length>20,shopify_source_present:!!c?.source_url&&(pj.product?.body_html||'').includes(c.source_url),shopify_meta_status:mr.status,shopify_meta_mfr_match:parsed?.manufacturer_sku===w.mfr_sku});
+}
+const badAuth=await fetch(`https://${domain}/admin/api/2024-10/shop.json`,{headers:{'X-Shopify-Access-Token':'invalid'}});
+const proof={ticket:'TK-11047',risk_tier:'R4',timestamp:new Date().toISOString(),result_path:resultPath,canonical_count:canonical.length,negative_auth_status:badAuth.status,checks};
+const out=path.join(ROOT,`data/carnegie-spec-E2E-${path.basename(resultPath).replace(/^carnegie-spec-bulk-RESULT-|\.json$/g,'')}.json`); fs.writeFileSync(out,JSON.stringify(proof,null,2));
+const bad=badAuth.status!==401||checks.some(x=>Object.entries(x).some(([k,v])=>k!=='shopify_id'&&k!=='shopify_status'&&k!=='shopify_meta_status'&&v!==true)||x.shopify_status!==200||x.shopify_meta_status!==200);
+console.log(JSON.stringify({proof:out,checks:checks.length,negative_auth_status:badAuth.status,bad},null,2)); if(bad)process.exit(2);
← 7564cd40 TK-11047 sync Carnegie Terrain A7 specs canary
·
back to Dw Validator Debug TK11314
·
auto-data-snapshot: 2026-08-31T13:51:21 (1 data files) — DW- 7fc79f2d →