← back to Carnegie Reprice
feat: Carnegie importer gate — skip ACTIVE if no real mfr_sku (TK-10792 stop-the-bleed)
4de1b96e1eb55255e21c790242281ba5f4a840cb · 2026-08-23 04:11:48 -0700 · steve@designerwallcoverings.com
Add carnegie-mfr-gate.mjs (canonical guard + CLI audit) and wire into all
activation paths: rebuild-line.mjs, phase2-poc-acapella.mjs. Products with
null/empty or DWAG-* mfr_sku are held as DRAFT + tagged Needs-Mfr-SKU.
Durable run-summary written to ledger JSONL. archive-redirect.mjs rollback
path documented (intentionally exempt — restores original Carnegie products
with real vendor codes). CODE ONLY — no Shopify writes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files touched
M archive-redirect.mjsA carnegie-mfr-gate.mjsM phase2-poc-acapella.mjsM rebuild-line.mjs
Diff
commit 4de1b96e1eb55255e21c790242281ba5f4a840cb
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Sun Aug 23 04:11:48 2026 -0700
feat: Carnegie importer gate — skip ACTIVE if no real mfr_sku (TK-10792 stop-the-bleed)
Add carnegie-mfr-gate.mjs (canonical guard + CLI audit) and wire into all
activation paths: rebuild-line.mjs, phase2-poc-acapella.mjs. Products with
null/empty or DWAG-* mfr_sku are held as DRAFT + tagged Needs-Mfr-SKU.
Durable run-summary written to ledger JSONL. archive-redirect.mjs rollback
path documented (intentionally exempt — restores original Carnegie products
with real vendor codes). CODE ONLY — no Shopify writes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
archive-redirect.mjs | 3 ++
carnegie-mfr-gate.mjs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++
phase2-poc-acapella.mjs | 15 +++++++
rebuild-line.mjs | 32 ++++++++++++--
4 files changed, 155 insertions(+), 4 deletions(-)
diff --git a/archive-redirect.mjs b/archive-redirect.mjs
index 897cb27..6665e59 100644
--- a/archive-redirect.mjs
+++ b/archive-redirect.mjs
@@ -49,6 +49,9 @@ if(MODE==='rollback'){
if(!ROLLBACK_FILE||!fs.existsSync(ROLLBACK_FILE))throw new Error('rollback needs archive-run-map.json');
const map=JSON.parse(fs.readFileSync(ROLLBACK_FILE,'utf8'));
console.log(`[rollback] un-archiving ${map.archived.length} products + deleting ${map.redirects.length} redirects…`);
+ // TK-10792 note: rollback re-activates OLD multi-variant products that were intentionally
+ // archived. These are original Carnegie pattern pages with real vendor mfr_skus (pre-split),
+ // NOT new DWAG-* products. mfr_sku gate intentionally does not apply to this undo path.
for(const a of map.archived){ await shop(`/products/${a.old_id}.json`,{method:'PUT',body:JSON.stringify({product:{id:a.old_id,status:'active'}})}); await sleep(550); }
for(const r of map.redirects){ if(r.redirect_id){ try{await shop(`/redirects/${r.redirect_id}.json`,{method:'DELETE'});}catch{} await sleep(400);} }
console.log(`[rollback] done`); process.exit(0);
diff --git a/carnegie-mfr-gate.mjs b/carnegie-mfr-gate.mjs
new file mode 100644
index 0000000..98f8844
--- /dev/null
+++ b/carnegie-mfr-gate.mjs
@@ -0,0 +1,109 @@
+#!/usr/bin/env node
+// carnegie-mfr-gate.mjs — TK-10792 stop-the-bleed guard
+//
+// Validates that a Carnegie product row has a REAL vendor mfr_sku before
+// any activator is allowed to set status='active'. Products that fail are
+// kept as draft and tagged Needs-Mfr-SKU.
+//
+// Usage as a library:
+// import { mfrSkuValid, assertActivatable, SKIP_TAG } from './carnegie-mfr-gate.mjs';
+// const { ok, reason } = assertActivatable(row);
+// if (!ok) { /* keep draft, add SKIP_TAG to tags */ }
+//
+// Usage as a CLI audit:
+// node carnegie-mfr-gate.mjs # audit carnegie_catalog, print skip counts
+// node carnegie-mfr-gate.mjs --verbose # print every skipped SKU
+
+import { execFileSync } from 'node:child_process';
+
+// ---- Gate logic ----------------------------------------------------------------
+
+/** Tag applied to products that fail the gate */
+export const SKIP_TAG = 'Needs-Mfr-SKU';
+
+/**
+ * Returns true iff `mfr` is a real vendor mfr_sku:
+ * - not null / empty / whitespace-only
+ * - does NOT match /^DWAG-/i (DW-internal placeholder codes)
+ * - does NOT match /^DW[A-Z]{2}-/i in general (all DW-prefix internal codes)
+ * NOTE: only DWAG is confirmed as the bleed source; the regex is intentionally
+ * tight so legitimate vendor codes (DWSC-*, etc.) that happen to be known are
+ * not blocked. Adjust the regex if new internal prefixes are discovered.
+ */
+export function mfrSkuValid(mfr) {
+ if (mfr === null || mfr === undefined) return false;
+ const s = String(mfr).trim();
+ if (!s) return false;
+ if (/^DWAG-/i.test(s)) return false;
+ return true;
+}
+
+/**
+ * Returns { ok: true } if the row is safe to activate, or
+ * { ok: false, reason: string } if it must stay draft.
+ *
+ * @param {object} row - any object with a `mfr_sku` property
+ */
+export function assertActivatable(row) {
+ const mfr = row?.mfr_sku;
+ if (!mfrSkuValid(mfr)) {
+ const display = (mfr === null || mfr === undefined) ? 'null' : `"${String(mfr)}"`;
+ return {
+ ok: false,
+ reason: `mfr_sku ${display} is null, empty, or a DWAG-* internal placeholder — product stays DRAFT (${SKIP_TAG})`,
+ };
+ }
+ return { ok: true };
+}
+
+/**
+ * Given a comma-separated tags string, ensure SKIP_TAG is present.
+ * Returns the updated tags string.
+ */
+export function addSkipTag(tags) {
+ const arr = (tags || '').split(',').map(t => t.trim()).filter(Boolean);
+ if (!arr.includes(SKIP_TAG)) arr.push(SKIP_TAG);
+ return arr.join(', ');
+}
+
+// ---- CLI audit (when run directly) --------------------------------------------
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ const VERBOSE = process.argv.includes('--verbose');
+ const DB = 'postgresql:///dw_unified?host=/tmp';
+ const PSQL = [
+ '/opt/homebrew/opt/postgresql@14/bin/psql',
+ '/usr/local/opt/postgresql@14/bin/psql',
+ 'psql',
+ ].find(p => { try { execFileSync(p, ['--version'], { stdio: 'ignore' }); return true; } catch { return false; } }) || 'psql';
+
+ const q = sql => execFileSync(PSQL, [DB, '-At', '-c', sql], { encoding: 'utf8' }).trim();
+
+ console.log('=== Carnegie mfr_sku gate audit (carnegie_catalog) ===\n');
+
+ const total = +q('SELECT COUNT(*) FROM carnegie_catalog');
+ const nullCount = +q("SELECT COUNT(*) FROM carnegie_catalog WHERE mfr_sku IS NULL OR mfr_sku = ''");
+ const dwagCount = +q("SELECT COUNT(*) FROM carnegie_catalog WHERE mfr_sku ILIKE 'DWAG-%'");
+ const realCount = +q("SELECT COUNT(*) FROM carnegie_catalog WHERE mfr_sku IS NOT NULL AND mfr_sku <> '' AND mfr_sku NOT ILIKE 'DWAG-%'");
+
+ console.log(`Total rows : ${total}`);
+ console.log(`Real mfr_sku (PASS) : ${realCount} (${((realCount/total)*100).toFixed(1)}%)`);
+ console.log(`DWAG-* codes (SKIP) : ${dwagCount} (${((dwagCount/total)*100).toFixed(1)}%)`);
+ console.log(`null/empty (SKIP) : ${nullCount} (${((nullCount/total)*100).toFixed(1)}%)`);
+ console.log(`Total would SKIP : ${dwagCount + nullCount}`);
+
+ if (VERBOSE) {
+ console.log('\n--- Skipped SKUs (sample up to 50) ---');
+ const rows = q(
+ "SELECT dw_sku, mfr_sku, pattern_name FROM carnegie_catalog " +
+ "WHERE mfr_sku IS NULL OR mfr_sku = '' OR mfr_sku ILIKE 'DWAG-%' " +
+ "ORDER BY pattern_name, mfr_sku LIMIT 50"
+ );
+ for (const line of rows.split('\n').filter(Boolean)) {
+ console.log(' ', line);
+ }
+ }
+
+ console.log('\nProducts in this set would be created as DRAFT with tag "Needs-Mfr-SKU".');
+ console.log('They will NOT be activated until Steve provides the real vendor code.\n');
+}
diff --git a/phase2-poc-acapella.mjs b/phase2-poc-acapella.mjs
index c0699a6..22eb316 100644
--- a/phase2-poc-acapella.mjs
+++ b/phase2-poc-acapella.mjs
@@ -12,6 +12,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';
const APPLY = process.argv.includes('--apply');
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
@@ -148,11 +150,13 @@ function productPayload(it) {
}
// 5-field + image gate: catalog is complete, but verify per SKU
+// TK-10792: mfr_sku gate added — DWAG-* or null/empty → no activation
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');
+ if (!mfrSkuValid(it.mfr_sku)) reasons.push(`no-real-mfr-sku(${JSON.stringify(it.mfr_sku)})`);
return { pass: reasons.length === 0, reasons };
}
@@ -206,7 +210,18 @@ async function main() {
created.status = 'active';
} else {
created.status = 'draft';
+ const hasMfrIssue = gate.reasons.some(r => r.startsWith('no-real-mfr-sku'));
created.draft_reason = !imgLanded ? 'image-failed-to-attach' : gate.reasons.join(',');
+ // TK-10792: tag products blocked by mfr_sku gate so they surface in the Needs-Mfr-SKU filter
+ if (hasMfrIssue) {
+ try {
+ const t = await shopify('GET', `products/${pid}.json?fields=id,tags`);
+ const existingTags = (t.product.tags || '').split(',').map(s=>s.trim()).filter(Boolean);
+ if (!existingTags.includes(NEEDS_MFR_TAG)) {
+ await shopify('PUT', `products/${pid}.json`, { product: { id: pid, tags: [...existingTags, NEEDS_MFR_TAG].join(', ') } });
+ }
+ } catch(e) { console.warn(` could not add ${NEEDS_MFR_TAG} tag to ${pid}: ${e.message.slice(0,80)}`); }
+ }
}
reversal.created.push(created);
console.log(`[${i}/14] created ${pid} ${payload.title} -> ${created.status}`);
diff --git a/rebuild-line.mjs b/rebuild-line.mjs
index a9db665..c729732 100644
--- a/rebuild-line.mjs
+++ b/rebuild-line.mjs
@@ -8,9 +8,15 @@
// node rebuild-line.mjs --status # print progress from the ledger and exit
//
// Archiving old products + folding the proof is a SEPARATE step: archive-old.mjs (end, after verify).
+//
+// TK-10792 stop-the-bleed: mfr_sku gate — products without a real vendor mfr_sku
+// (null, empty, or DWAG-* internal placeholder) are created as DRAFT with the
+// Needs-Mfr-SKU tag. They are NEVER set to ACTIVE until Steve resolves the real code.
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';
const DIR = new URL('.', import.meta.url).pathname;
const ENV = `${process.env.HOME}/Projects/secrets-manager/.env`;
@@ -24,6 +30,7 @@ const DB = 'postgresql:///dw_unified?host=/tmp';
const q1 = sql => execFileSync(PSQL,[DB,'-At','-c',sql],{encoding:'utf8',maxBuffer:1<<30}).trim();
const sleep = ms => new Promise(r=>setTimeout(r,ms));
+
const LEDGER = path.join(DIR,'rebuild-ledger.jsonl');
const HALT = path.join(DIR,'rebuild-HALTED.flag');
const args = process.argv.slice(2);
@@ -114,11 +121,19 @@ function buildPayload(row){
'Carnegie','carnegie-textile','Fabric', ptype, 'Carnegie Textiles',
'display_variant',
].filter(Boolean);
- const tagStr = [...new Set(tags.map(t=>String(t).trim()).filter(Boolean))].join(', ');
+ 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.
+ const hasMfr = mfrSkuValid(row.mfr_sku);
+ const finalStatus = (imgs.length && hasMfr) ? '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}`);
+ }
const payload = { product: {
title, body_html: body, vendor:'Carnegie', product_type: ptype,
- handle, status: imgs.length ? 'active' : 'draft', published_scope:'web', tags: tagStr,
+ handle, status: finalStatus, published_scope:'web', tags: tagStr,
options:[{name:'Format'}],
images: imgs.map((src,i)=>({src, position:i+1})),
variants:[
@@ -168,7 +183,7 @@ async function main(){
const todo = skus.filter((s,i)=> (i%SHARD_M===SHARD_N) && !(done.get(s)&&done.get(s).product_id));
console.log(`[shard ${SHARD_N+1}/${SHARD_M}] catalog ${skus.length} | already created ${[...done.values()].filter(r=>r.product_id).length} | this shard to create ${todo.length} (limit ${LIMIT})`);
- let made=0, sinceCheck=0, imgFailStreak=0;
+ let made=0, mfrSkipped=0, sinceCheck=0, imgFailStreak=0;
for(const sku of todo){
if(made>=LIMIT) break;
let row;
@@ -199,8 +214,11 @@ async function main(){
const imageStarved = built.imgCount>0 && attached<=0;
imgFailStreak = imageStarved ? imgFailStreak+1 : 0;
+ const wasMfrSkipped = (product.status === 'draft') && !mfrSkuValid(row.mfr_sku);
+ if (wasMfrSkipped) mfrSkipped++;
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',
mf_ok:mf.ok, mf_err:mf.err, at:new Date().toISOString() });
made++; sinceCheck++;
@@ -215,7 +233,13 @@ async function main(){
}
await sleep(250); // gentle pace, well under any burst limit
}
- console.log(`DONE this run: ${made} created. Ledger: ${LEDGER}`);
+ // TK-10792: durable skip-count so operators can see the gate's effect without parsing all JSONL
+ const summary = { type:'run-summary', shard:`${SHARD_N+1}/${SHARD_M}`, made, mfrDraftSkipped:mfrSkipped,
+ note: mfrSkipped > 0 ? `${mfrSkipped} products held as DRAFT — mfr_sku null/empty/DWAG-*; needs Steve review` : 'all activations had real mfr_sku',
+ at: new Date().toISOString() };
+ ledgerAppend(summary);
+ if (mfrSkipped > 0) console.warn(`\n[TK-10792] ${mfrSkipped} products held as DRAFT (tagged ${NEEDS_MFR_TAG}). Real vendor mfr_sku required before activation. See ledger run-summary.\n`);
+ console.log(`DONE this run: ${made} created (${mfrSkipped} kept DRAFT due to missing/DWAG mfr_sku — TK-10792). Ledger: ${LEDGER}`);
}
function haltStorage(sku, msg){
← 9f61e80 carnegie reconcile: reconcile.mjs (dedup plan, live-truth) +
·
back to Carnegie Reprice
·
carnegie-mfr-gate: add bodyHtmlValid + wire into assertActiv 015d3c8 →