← back to Carnegie Reprice
carnegie phase2: retry metafield writes, record failures, guard hardcoded RETAIL
5f484b7247f5a72a96c34251e28184affb0757aa · 2026-09-24 11:36:23 -0700 · Steve Abrams
Apply-path hardening on the Acapella PoC: (1) shopify() now retries 429/5xx with
backoff so per-product metafield writes aren't silently dropped on a transient
throttle; (2) failed metafields are recorded on the created record (created.mf_failed
-> run summary) instead of vanishing into a console.warn; (3) added a guard that
aborts if any item's cost != $42, since RETAIL=76 is hardcoded from that cost and a
copy to a different-cost pattern would otherwise ship a wrong price. Also lands the
reviewed-clean TK-10820 change (activation gates on bodyHtmlValid, not raw
description_text) that was uncommitted in this file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M phase2-poc-acapella.mjs
Diff
commit 5f484b7247f5a72a96c34251e28184affb0757aa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 11:36:23 2026 -0700
carnegie phase2: retry metafield writes, record failures, guard hardcoded RETAIL
Apply-path hardening on the Acapella PoC: (1) shopify() now retries 429/5xx with
backoff so per-product metafield writes aren't silently dropped on a transient
throttle; (2) failed metafields are recorded on the created record (created.mf_failed
-> run summary) instead of vanishing into a console.warn; (3) added a guard that
aborts if any item's cost != $42, since RETAIL=76 is hardcoded from that cost and a
copy to a different-cost pattern would otherwise ship a wrong price. Also lands the
reviewed-clean TK-10820 change (activation gates on bodyHtmlValid, not raw
description_text) that was uncommitted in this file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
phase2-poc-acapella.mjs | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/phase2-poc-acapella.mjs b/phase2-poc-acapella.mjs
index 22eb316..126194f 100644
--- a/phase2-poc-acapella.mjs
+++ b/phase2-poc-acapella.mjs
@@ -13,7 +13,8 @@ import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
// TK-10792: canonical mfr_sku gate
-import { mfrSkuValid, SKIP_TAG as NEEDS_MFR_TAG } from './carnegie-mfr-gate.mjs';
+// TK-10820: also import bodyHtmlValid so a blank rendered body can never go ACTIVE via this path
+import { mfrSkuValid, bodyHtmlValid, SKIP_TAG as NEEDS_MFR_TAG } from './carnegie-mfr-gate.mjs';
const APPLY = process.argv.includes('--apply');
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
@@ -34,7 +35,7 @@ function psqlJson(sql) {
return JSON.parse(out.trim() || '[]');
}
-async function shopify(method, endpoint, body) {
+async function shopify(method, endpoint, body, tries = 0) {
const res = await fetch(`https://${DOMAIN}/admin/api/${API}/${endpoint}`, {
method,
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
@@ -42,6 +43,12 @@ async function shopify(method, endpoint, body) {
});
const txt = await res.text();
let json; try { json = JSON.parse(txt); } catch { json = { raw: txt }; }
+ // Retry transient throttle/5xx with backoff so metafield writes aren't silently
+ // dropped when Shopify's REST bucket is momentarily exhausted.
+ if ((res.status === 429 || res.status >= 500) && tries < 6) {
+ await new Promise(r => setTimeout(r, 1500 * (tries + 1)));
+ return shopify(method, endpoint, body, tries + 1);
+ }
if (!res.ok) throw new Error(`${method} ${endpoint} -> ${res.status}: ${txt.slice(0, 300)}`);
return json;
}
@@ -77,6 +84,11 @@ function buildTitle(it) {
}
const RETAIL = 76; // round(42/0.65/0.85)
+// RETAIL is hardcoded from cost=$42. If this PoC is copied to a Carnegie pattern
+// with a different cost, that assumption ships a silently-wrong price — so assert
+// every item really is cost $42 before using the hardcoded retail.
+const offCost = items.filter(it => Number(it.price) !== 42);
+if (offCost.length) { console.error(`RETAIL=${RETAIL} assumes cost $42, but ${offCost.length} item(s) differ (e.g. ${offCost[0].dw_sku}=$${offCost[0].price}) — aborting; recompute RETAIL per item`); process.exit(1); }
function specMetafields(it) {
// house specs live in global.* (copied from Kravet reference)
@@ -155,7 +167,10 @@ function gatePass(it, imgSrc) {
const reasons = [];
if (!imgSrc && !it.local_image) reasons.push('no-image');
if (!it.width) reasons.push('no-width');
- if (!it.description_text) reasons.push('no-desc');
+ // TK-10820: gate the RENDERED body (payload body_html = description_text || ''),
+ // stripping tags/ /whitespace — matches the create-path gate in rollout.mjs so an
+ // empty <p></p> / whitespace-only description can never slip through as ACTIVE.
+ if (!bodyHtmlValid(it.description_text)) reasons.push('no-desc');
if (!mfrSkuValid(it.mfr_sku)) reasons.push(`no-real-mfr-sku(${JSON.stringify(it.mfr_sku)})`);
return { pass: reasons.length === 0, reasons };
}
@@ -199,10 +214,13 @@ async function main() {
variant_ids: res.product.variants.map(v => ({ sku: v.sku, id: v.id, price: v.price })),
image_ok: (res.product.images || []).length > 0 };
// metafields
+ const mfFailed = [];
for (const m of specMetafields(it)) {
try { await shopify('POST', `products/${pid}/metafields.json`, { metafield: m }); }
- catch (e) { console.warn(` mf ${m.namespace}.${m.key} failed: ${e.message.slice(0,80)}`); }
+ catch (e) { mfFailed.push({ ns: m.namespace, key: m.key, err: e.message.slice(0,120) }); console.warn(` mf ${m.namespace}.${m.key} failed: ${e.message.slice(0,80)}`); }
}
+ // Surface dropped metafields in the run record instead of only console.warn.
+ if (mfFailed.length) { created.mf_failed = mfFailed; console.warn(` ⚠ ${mfFailed.length} metafield(s) failed for ${it.dw_sku} — recorded in summary`); }
// activate if gate passes AND image landed
const imgLanded = (res.product.images || []).length > 0;
if (gate.pass && imgLanded) {
← 081afea TK-11547: weight guard on the carnegie rollback un-archive (
·
back to Carnegie Reprice
·
auto-data-snapshot: 2026-09-24T12:18:37 (1 data files) — pha 21bd662 →