[object Object]

← 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

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/&nbsp;/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 →