← back to Carnegie Reprice
TK-10820: gate rebuild-line + build-one-proof born-active on non-blank body_html
618df8bc65dfa256927af9c041dbd022b23f7d9f · 2026-08-24 19:23:54 -0700 · Steve Abrams
HOLE 1 (Cody): rebuild-line.mjs (the 5,928-SKU Phase-3 rollout create path) gated
ACTIVE only on (imgs.length && hasMfr) — a scraped body that is a whitespace/entity
shell (e.g. <p> </p>) is truthy at L100 so the fallback composer never fires,
letting a blank-desc product ship ACTIVE. Now imports bodyHtmlValid and gates on
(imgs.length && hasMfr && bodyValid); blank body -> DRAFT + Needs-Description tag,
recorded in the ledger (desc_gate field).
Also patched build-one-proof.mjs (the 7th born-active create path Cody's census asked
me to find): it POSTed status:'active' with raw row.description_text and NO gate. Now
gates on imgs.length && mfrSkuValid && bodyHtmlValid, DRAFT+tag otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M build-one-proof.mjsM rebuild-line.mjs
Diff
commit 618df8bc65dfa256927af9c041dbd022b23f7d9f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 24 19:23:54 2026 -0700
TK-10820: gate rebuild-line + build-one-proof born-active on non-blank body_html
HOLE 1 (Cody): rebuild-line.mjs (the 5,928-SKU Phase-3 rollout create path) gated
ACTIVE only on (imgs.length && hasMfr) — a scraped body that is a whitespace/entity
shell (e.g. <p> </p>) is truthy at L100 so the fallback composer never fires,
letting a blank-desc product ship ACTIVE. Now imports bodyHtmlValid and gates on
(imgs.length && hasMfr && bodyValid); blank body -> DRAFT + Needs-Description tag,
recorded in the ledger (desc_gate field).
Also patched build-one-proof.mjs (the 7th born-active create path Cody's census asked
me to find): it POSTed status:'active' with raw row.description_text and NO gate. Now
gates on imgs.length && mfrSkuValid && bodyHtmlValid, DRAFT+tag otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
build-one-proof.mjs | 18 +++++++++++++++---
rebuild-line.mjs | 17 +++++++++++++----
2 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/build-one-proof.mjs b/build-one-proof.mjs
index c2033ea..6f8cc7a 100644
--- a/build-one-proof.mjs
+++ b/build-one-proof.mjs
@@ -2,6 +2,9 @@
// from the now-complete carnegie_catalog, to prove what the rebuild looks like.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
+// TK-10820: canonical born-active guards — a proof product is still a born-active
+// create path, so it must obey the same body_html + mfr_sku gate as every rollout path.
+import { mfrSkuValid, bodyHtmlValid, SKIP_TAG as NEEDS_MFR_TAG, NEEDS_DESC_TAG } from './carnegie-mfr-gate.mjs';
const ENV = `${process.env.HOME}/Projects/secrets-manager/.env`;
const env = k => { const m = fs.readFileSync(ENV,'utf8').split('\n').find(l=>l.startsWith(k+'=')); return m? m.slice(k.length+1).trim().replace(/^["']|["']$/g,''):''; };
const TOKEN = env('SHOPIFY_ADMIN_TOKEN'); let SHOP = env('SHOPIFY_STORE_DOMAIN')||env('SHOPIFY_STORE'); if(SHOP&&!SHOP.includes('.'))SHOP+='.myshopify.com';
@@ -20,11 +23,20 @@ const big = u => u? u.replace(/height=\d*&width=\d*/,'height=1400&width=1400').r
const imgs = (row.all_images||[]).filter(u=>/_puf|_pud|siltech/i.test(u)).slice(0,3).map(big);
// --- create product (unlinked proof) ---
+// TK-10820: gate born-active on real mfr_sku AND non-blank body_html (a whitespace/entity
+// shell must never ship ACTIVE). A proof that fails either stays DRAFT + the matching tag.
+const proofBody = row.description_text;
+const hasMfr = mfrSkuValid(row.mfr_sku);
+const bodyValid = bodyHtmlValid(proofBody);
+const finalStatus = (imgs.length && hasMfr && bodyValid) ? 'active' : 'draft';
+if(!hasMfr) console.warn(`[MFR-GATE] ${SKU} mfr=${JSON.stringify(row.mfr_sku)} → DRAFT+${NEEDS_MFR_TAG}`);
+if(!bodyValid) console.warn(`[DESC-GATE] ${SKU} blank body_html → DRAFT+${NEEDS_DESC_TAG}`);
const title = `Carnegie Siltech Grain — ${colorName}`;
const payload = { product: {
- title, body_html: row.description_text, vendor:'Carnegie', product_type:'Upholstery',
- handle: 'carnegie-siltech-grain-proof-ivory', status:'active', published_scope:'web',
- tags: [colorName, row.color_bucket, 'Carnegie','Fabric','Upholstery','Carnegie Textiles','PROOF-DELETE-ME'].filter(Boolean).join(', '),
+ title, body_html: proofBody, vendor:'Carnegie', product_type:'Upholstery',
+ handle: 'carnegie-siltech-grain-proof-ivory', status: finalStatus, published_scope:'web',
+ tags: [colorName, row.color_bucket, 'Carnegie','Fabric','Upholstery','Carnegie Textiles','PROOF-DELETE-ME',
+ ...(!hasMfr?[NEEDS_MFR_TAG]:[]), ...(!bodyValid?[NEEDS_DESC_TAG]:[])].filter(Boolean).join(', '),
options:[{name:'Format'}],
images: imgs.map((src,i)=>({src, position:i+1})),
variants:[
diff --git a/rebuild-line.mjs b/rebuild-line.mjs
index c729732..2ee8389 100644
--- a/rebuild-line.mjs
+++ b/rebuild-line.mjs
@@ -16,7 +16,7 @@ import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
// TK-10792: canonical mfr_sku gate — one source of truth, no inline copy
-import { mfrSkuValid, addSkipTag, SKIP_TAG as NEEDS_MFR_TAG } from './carnegie-mfr-gate.mjs';
+import { mfrSkuValid, bodyHtmlValid, addSkipTag, SKIP_TAG as NEEDS_MFR_TAG, NEEDS_DESC_TAG } from './carnegie-mfr-gate.mjs';
const DIR = new URL('.', import.meta.url).pathname;
const ENV = `${process.env.HOME}/Projects/secrets-manager/.env`;
@@ -123,13 +123,20 @@ function buildPayload(row){
].filter(Boolean);
let tagStr = [...new Set(tags.map(t=>String(t).trim()).filter(Boolean))].join(', ');
- // TK-10792: only allow ACTIVE if images exist AND mfr_sku is a real vendor code.
+ // TK-10792/TK-10820: only allow ACTIVE if images exist AND mfr_sku is a real vendor code
+ // AND the final body_html is a real, non-blank description (a whitespace/entity shell like
+ // <p> </p> is truthy at L100 so the fallback composer never fires — this catches it).
const hasMfr = mfrSkuValid(row.mfr_sku);
- const finalStatus = (imgs.length && hasMfr) ? 'active' : 'draft';
+ const bodyValid = bodyHtmlValid(body);
+ const finalStatus = (imgs.length && hasMfr && bodyValid) ? 'active' : 'draft';
if (!hasMfr) {
tagStr = addSkipTag(tagStr);
console.warn(` [MFR-GATE] SKIP-ACTIVE dw_sku=${row.dw_sku} mfr_sku=${JSON.stringify(row.mfr_sku)} — keeping DRAFT+${NEEDS_MFR_TAG}`);
}
+ if (!bodyValid) {
+ if (!tagStr.split(',').map(t=>t.trim()).includes(NEEDS_DESC_TAG)) tagStr = [tagStr, NEEDS_DESC_TAG].filter(Boolean).join(', ');
+ console.warn(` [DESC-GATE] SKIP-ACTIVE dw_sku=${row.dw_sku} — blank body_html — keeping DRAFT+${NEEDS_DESC_TAG}`);
+ }
const payload = { product: {
title, body_html: body, vendor:'Carnegie', product_type: ptype,
@@ -155,7 +162,7 @@ function buildPayload(row){
pattern_name:row.pattern_name, color:colorName, color_number:String(row.color_number||''),
product_class:'Fabric', collection_name:'Carnegie Textiles' };
- return { payload, spec, ident, colorName, retail, handle, imgCount: imgs.length, primary_hex: row.primary_hex };
+ return { payload, spec, ident, colorName, retail, handle, imgCount: imgs.length, bodyValid, primary_hex: row.primary_hex };
}
// REST metafields — preserves the EXACT space-containing keys the proof rendered with
@@ -216,9 +223,11 @@ async function main(){
const wasMfrSkipped = (product.status === 'draft') && !mfrSkuValid(row.mfr_sku);
if (wasMfrSkipped) mfrSkipped++;
+ const bodyGateHeld = (product.status === 'draft') && !built.bodyValid;
ledgerAppend({ dw_sku:sku, product_id:product.id, handle:built.handle, color:built.colorName,
retail:built.retail, imgExpected:built.imgCount, imgAttached:attached, status:product.status,
mfr_sku:row.mfr_sku, mfr_gate: wasMfrSkipped ? 'DRAFT-no-mfr' : 'ok',
+ desc_gate: bodyGateHeld ? 'DRAFT-blank-desc' : 'ok',
mf_ok:mf.ok, mf_err:mf.err, at:new Date().toISOString() });
made++; sinceCheck++;
← 83498d8 carnegie-mfr-gate: broaden bodyHtmlValid entity strip (Cody
·
back to Carnegie Reprice
·
auto-data-snapshot: 2026-09-10T14:13:31 (4 data files) — pla bc0fdf7 →