← back to Dw Activation Debug TK11314
feat(golive-gate): mfr_sku provenance gate blocks blank/fabricated codes at DRAFT->ACTIVE
99013ea3e4c28d90e9bacc134343f997a00e927b · 2026-09-01 08:02:16 -0700 · Steve Abrams
Adds lib/mfr-gate.js (pure, tested) + lib/mfr-gate-resolve.js (dw_unified I/O),
wired into rotate-activate.js / mdc-activate.js / cmo-activate.js before the flip.
Blocks: blank/null/unknown mfr, cross-vendor reused counters, and fabricated
<base>+<seq> runs (Carnegie Grain 6300->63001.. via placeholder 'Color N' staging).
Blocked drafts tag Needs-MfrSKU + hold; by-colour lines (Novasuede) exempt. TK-11063.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M cmo-activate.jsA lib/mfr-gate-resolve.jsA lib/mfr-gate.jsM mdc-activate.jsM rotate-activate.jsA test/mfr-gate.test.js
Diff
commit 99013ea3e4c28d90e9bacc134343f997a00e927b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 08:02:16 2026 -0700
feat(golive-gate): mfr_sku provenance gate blocks blank/fabricated codes at DRAFT->ACTIVE
Adds lib/mfr-gate.js (pure, tested) + lib/mfr-gate-resolve.js (dw_unified I/O),
wired into rotate-activate.js / mdc-activate.js / cmo-activate.js before the flip.
Blocks: blank/null/unknown mfr, cross-vendor reused counters, and fabricated
<base>+<seq> runs (Carnegie Grain 6300->63001.. via placeholder 'Color N' staging).
Blocked drafts tag Needs-MfrSKU + hold; by-colour lines (Novasuede) exempt. TK-11063.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
cmo-activate.js | 21 ++++++++----
lib/mfr-gate-resolve.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++
lib/mfr-gate.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++
mdc-activate.js | 18 +++++++---
rotate-activate.js | 42 ++++++++++++++++++++---
test/mfr-gate.test.js | 69 +++++++++++++++++++++++++++++++++++++
6 files changed, 314 insertions(+), 14 deletions(-)
diff --git a/cmo-activate.js b/cmo-activate.js
index 6409933..462b48b 100644
--- a/cmo-activate.js
+++ b/cmo-activate.js
@@ -3,6 +3,8 @@
const fs=require('fs'), os=require('os'), path=require('path');
const { SettlementGate } = require('./lib/settlement-gate.js');
const { fiveFieldExtra } = require('./lib/five-field-extra.js');
+const { mfrGate } = require('./lib/mfr-gate.js');
+const { reusedMfrSet } = require('./lib/mfr-gate-resolve.js');
const { validateBeforeActivate } = require(path.join(os.homedir(),'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
const { execSync } = require('child_process');
const TOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1];
@@ -14,11 +16,14 @@ async function gql(q,v){ for(let a=0;a<4;a++){ try{ const r=await fetch(`https:/
const L=fs.createWriteStream('/tmp/cmo_activate.log',{flags:'w'});
const P=m=>{L.write(m+'\n');console.log(m);};
-// material map from staging (helps settlement auto-pass textures)
-const matMap={};
-for(const line of execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT mfr_sku,material FROM cmo_paris_catalog;"`).toString().trim().split('\n')){
- const [s,m]=line.split('\t'); if(s) matMap[s]=m||'';
+// material + color map from staging (material helps settlement auto-pass textures;
+// color_name is the fabricated-placeholder signal for the mfr-provenance gate, TK-11063)
+const matMap={}, colorMap={};
+for(const line of execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT mfr_sku,coalesce(material,''),coalesce(color_name,'') FROM cmo_paris_catalog;"`).toString().trim().split('\n')){
+ const [s,m,c]=line.split('\t'); if(s){ matMap[s]=m||''; colorMap[s]=c||''; }
}
+// cross-vendor reused-code set (internal-counter bug class), loaded once.
+const REUSED_MFRS=reusedMfrSet();
const mfVal=(mfs,ns,k)=>{const x=mfs.find(m=>m.namespace===ns&&m.key===k);return x?x.value:'';};
async function loadPubs(){ const r=await gql(`{publications(first:50){edges{node{id name}}}}`,{}); return (r.data?.publications?.edges||[]).map(e=>e.node).filter(p=>p.id!==GOOGLE); }
@@ -28,7 +33,7 @@ async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsS
const gate=new SettlementGate(); P(`settlement lock: ${JSON.stringify(gate.lockStatus())} mode=${DRY?'DRY':'COMMIT'}`);
const pubs=DRY?[]:await loadPubs(); if(!DRY)P(`publications (ex-Google): ${pubs.map(p=>p.name).join(', ')}`);
const Q=`query($cur:String){products(first:40,query:"vendor:'CMO Paris' status:draft",after:$cur){pageInfo{hasNextPage endCursor} nodes{id title tags descriptionHtml featuredImage{url} images(first:6){nodes{url}} variants(first:5){nodes{sku price}} metafields(first:40){nodes{namespace key value}}}}}`;
- let cur=null, scanned=0, activated=0, publishedN=0, heldSettle=0, heldValidate=0, blocked=0, cost=0;
+ let cur=null, scanned=0, activated=0, publishedN=0, heldSettle=0, heldValidate=0, blocked=0, heldMfr=0, cost=0;
do{
const r=await gql(Q,{cur});
const pr=r.data?.products; if(!pr){P('load error: '+JSON.stringify(r).slice(0,200));break;}
@@ -39,6 +44,10 @@ async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsS
const mfr=mfVal(mfs,'custom','manufacturer_sku');
const imgs=(n.images.nodes||[]).map(i=>i.url);
const width=mfVal(mfs,'global','width');
+ // MFR-PROVENANCE gate (TK-11063) — BLOCK blank/reused/fabricated mfr before customer-facing.
+ const mfrCode=(mfVal(mfs,'dwc','manufacturer_sku')||mfr||mfVal(mfs,'global','manufacturer_sku')||'').trim();
+ const mg=mfrGate({vendor:'CMO Paris',mfr:mfrCode,reusedAcrossVendors:!!mfrCode&&REUSED_MFRS.has(mfrCode),stagingColorName:colorMap[mfrCode]||null});
+ if(!mg.ok){ heldMfr++; P(`HOLD(mfr:${mg.reasons.join(',')}) ${dwSku} ${mfrCode||'(blank)'}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Needs-MfrSKU']}); continue; }
// 5-field / specs gate (real validator)
const val=validateBeforeActivate({title:n.title,dwSku,vendor:'CMO Paris',tags:n.tags||[],
descriptionHtml:n.descriptionHtml,variants:n.variants.nodes,
@@ -69,5 +78,5 @@ async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsS
}
cur=pr.pageInfo.hasNextPage?pr.pageInfo.endCursor:null;
}while(cur);
- P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} activated=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate} settlement_cost=$${cost.toFixed(4)}`);
+ P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} activated=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate} heldMfr=${heldMfr} settlement_cost=$${cost.toFixed(4)}`);
})();
diff --git a/lib/mfr-gate-resolve.js b/lib/mfr-gate-resolve.js
new file mode 100644
index 0000000..22bc88b
--- /dev/null
+++ b/lib/mfr-gate-resolve.js
@@ -0,0 +1,91 @@
+'use strict';
+
+/*
+ * mfr-gate-resolve.js — the thin I/O layer that feeds lib/mfr-gate.js.
+ *
+ * Keeps mfr-gate.js pure (no DB) by doing the cheap dw_unified lookups here:
+ * - reusedMfrSet(): one query → the SET of mfr codes that appear under >1 vendor
+ * (the internal-counter bug class), loaded ONCE per activator run.
+ * - stagingColorFor(vendor, mfr): resolve the vendor's staging table via
+ * vendor_registry.catalog_table and read the color_name of the row whose
+ * mfr_sku === the resolved code (the fabricated-placeholder-color signal).
+ *
+ * All reads are host=/tmp local dw_unified. Fail-safe: any query error returns the
+ * EMPTY/absent value so the gate fails OPEN on that dimension (never a false BLOCK
+ * on a DB hiccup; the gate only BLOCKs on positive evidence).
+ */
+const { execFileSync } = require('child_process');
+
+function psql(sql) {
+ return execFileSync('psql', ['host=/tmp dbname=dw_unified', '-tAF', '\t', '-c', sql],
+ { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+}
+
+// SQL string literal escaper (single-quote doubling). Codes/vendors are alnum+dash in
+// practice, but never interpolate raw.
+const lit = (s) => `'${String(s == null ? '' : s).replace(/'/g, "''")}'`;
+
+// The set of mfr codes shared across >1 vendor in shopify_products — the reused
+// internal-counter signature. Loaded once; O(1) membership after. Fail-safe → empty set.
+function reusedMfrSet() {
+ try {
+ const raw = psql(`
+ SELECT rm FROM (
+ SELECT COALESCE(
+ NULLIF(metafields->'dwc'->'manufacturer_sku'->>'value',''),
+ NULLIF(metafields->'custom'->'manufacturer_sku'->>'value',''),
+ NULLIF(metafields->'global'->'manufacturer_sku'->>'value',''),
+ NULLIF(mfr_sku,'')
+ ) AS rm, vendor
+ FROM shopify_products
+ ) q
+ WHERE rm IS NOT NULL
+ GROUP BY rm HAVING count(DISTINCT vendor) > 1;`);
+ return new Set(raw ? raw.split('\n').map((l) => l.trim()).filter(Boolean) : []);
+ } catch (_) {
+ return new Set();
+ }
+}
+
+// Cache vendor→catalog_table so we resolve vendor_registry once per vendor per run.
+const _tableCache = new Map();
+function catalogTableFor(vendor) {
+ const key = String(vendor || '').toLowerCase();
+ if (_tableCache.has(key)) return _tableCache.get(key);
+ let table = null;
+ try {
+ // Exact-name first, then a contained match (vendor labels vary slightly). Only
+ // accept a real, safe table identifier (letters/digits/underscore) — never a value
+ // we'd interpolate blindly into the next query.
+ const raw = psql(
+ `SELECT catalog_table FROM vendor_registry
+ WHERE catalog_table IS NOT NULL AND catalog_table <> ''
+ AND (lower(vendor_name) = ${lit(key)} OR lower(vendor_name) LIKE ${lit('%' + key + '%')})
+ ORDER BY (lower(vendor_name) = ${lit(key)}) DESC LIMIT 1;`);
+ const t = (raw || '').split('\n')[0].trim();
+ if (/^[a-z_][a-z0-9_]*$/i.test(t)) table = t;
+ } catch (_) { /* fail-open */ }
+ _tableCache.set(key, table);
+ return table;
+}
+
+// The color_name of the staging row whose mfr_sku === the resolved code. Returns null
+// when there is no staging table, no matching row, or on any error → gate fails open
+// on the fabricated-sequence dimension. Guards that the table actually has the columns.
+function stagingColorFor(vendor, mfr) {
+ if (!mfr) return null;
+ const table = catalogTableFor(vendor);
+ if (!table) return null;
+ try {
+ const raw = psql(
+ `SELECT color_name FROM ${table}
+ WHERE mfr_sku = ${lit(mfr)} AND color_name IS NOT NULL
+ LIMIT 1;`);
+ const c = (raw || '').split('\n')[0];
+ return c === '' ? null : c;
+ } catch (_) {
+ return null; // e.g. table lacks color_name/mfr_sku columns — fail open
+ }
+}
+
+module.exports = { reusedMfrSet, catalogTableFor, stagingColorFor };
diff --git a/lib/mfr-gate.js b/lib/mfr-gate.js
new file mode 100644
index 0000000..92d4c82
--- /dev/null
+++ b/lib/mfr-gate.js
@@ -0,0 +1,87 @@
+'use strict';
+
+/*
+ * mfr-gate.js — manufacturer-SKU PROVENANCE gate for the DRAFT→ACTIVE flip.
+ *
+ * Steve's HARD go-live rule (memory new-sku-vendor-mfr-gate, 2026-07-06): any SKU
+ * that goes live must carry a REAL manufacturer SKU number — never blank, never a
+ * reused internal counter, never an AI/importer-fabricated sequence. The
+ * dw-golive-gate-canary MONITORS this after the fact; this gate ENFORCES it BEFORE
+ * activation so the activator stops flipping non-compliant products live (TK-11063).
+ *
+ * Mirrors the canary's predicate (~/.claude/skills/dw-golive-gate-canary/auditor.mjs)
+ * and extends it with the single-vendor FABRICATED-sequence class the canary's
+ * cross-vendor-reuse test can't see (the Carnegie "Grain 6300 → 63001..630026" bug,
+ * where the per-color join invented a 1..N counter and never captured a real color).
+ *
+ * DESIGN — pure + testable like five-field-extra.js. `mfrGate(ctx)` takes the
+ * already-resolved facts (no I/O) so it unit-tests cleanly; the activator does the
+ * cheap DB/metafield resolution and hands the result in. Fail-safe: this can only
+ * BLOCK an activation, never cause one. Fail-OPEN on unknowns (blank/reuse are the
+ * high-confidence classes and are covered explicitly; the fabricated-sequence class
+ * only blocks on POSITIVE evidence from staging, never on absence of it).
+ */
+
+// A staging color_name that is just a placeholder ("Color 1", "Color 12", "colour 3")
+// = DW never captured a real color for this SKU. Per the title rule (color from a real
+// scrape only, never AI) this is itself a provenance failure, and it is the signature
+// of the fabricated <base>+<seq> family (each fabricated member got a "Color N" stub).
+const PLACEHOLDER_COLOR_RE = /^colou?r\s*\d+$/i;
+
+// By-COLOUR private lines that legitimately have NO vendor mfr# (Steve, 2026-08-12) —
+// they should stay LIVE, not be blocked. Kept in sync with the canary's
+// NOMFR_EXEMPT_VENDORS. Extend for future by-colour lines.
+const NOMFR_EXEMPT_VENDORS = ['novasuede'];
+
+function isExemptVendor(vendor) {
+ const v = String(vendor || '').toLowerCase();
+ return NOMFR_EXEMPT_VENDORS.some((e) => v.includes(e));
+}
+
+/*
+ * mfrGate(ctx) → { ok, reasons }
+ *
+ * ctx:
+ * vendor {string} the product's vendor (for the by-colour exemption)
+ * mfr {string} the RESOLVED mfr code (metafield dwc/custom/global
+ * .manufacturer_sku, else the mfr_sku column). '' / null
+ * / 'unknown' all count as blank.
+ * reusedAcrossVendors {bool} true iff this exact code appears under >1 vendor in
+ * shopify_products (the internal-counter bug class).
+ * stagingColorName {string|null|undefined} the color_name of the staging row whose
+ * mfr_sku === this code, or null/undefined if there is no
+ * such row / staging was not consulted. A PLACEHOLDER value
+ * ("Color N") = fabricated provenance → BLOCK. A real color
+ * name, or no staging row at all, does NOT block here.
+ */
+function mfrGate(ctx) {
+ const ctxObj = ctx && typeof ctx === 'object' ? ctx : {};
+ const vendor = ctxObj.vendor;
+ const reasons = [];
+
+ // By-colour private lines are a legitimate exception — never a mfr FAIL.
+ if (isExemptVendor(vendor)) return { ok: true, reasons };
+
+ const mfr = String(ctxObj.mfr == null ? '' : ctxObj.mfr).trim();
+
+ // (a) blank / null / literal "unknown" (never allowed in a title either).
+ if (!mfr || /^unknown$/i.test(mfr)) {
+ reasons.push('mfr-sku-blank');
+ return { ok: false, reasons }; // nothing else to test on a blank code
+ }
+
+ // (b) reused internal counter — the same code under >1 vendor.
+ if (ctxObj.reusedAcrossVendors === true) reasons.push('mfr-sku-reused-across-vendors');
+
+ // (c) fabricated single-vendor sequence — POSITIVE evidence only: the staging row
+ // for this exact code carries a placeholder "Color N" name (the invented-counter
+ // signature). Absence of a staging row, or a real color name, does NOT block.
+ const staging = ctxObj.stagingColorName;
+ if (typeof staging === 'string' && PLACEHOLDER_COLOR_RE.test(staging.trim())) {
+ reasons.push('mfr-sku-fabricated-placeholder-color');
+ }
+
+ return { ok: reasons.length === 0, reasons };
+}
+
+module.exports = { mfrGate, isExemptVendor, NOMFR_EXEMPT_VENDORS, PLACEHOLDER_COLOR_RE };
diff --git a/mdc-activate.js b/mdc-activate.js
index 6fe7614..3a4885e 100644
--- a/mdc-activate.js
+++ b/mdc-activate.js
@@ -27,6 +27,8 @@
const fs=require('fs'), os=require('os'), path=require('path');
const { SettlementGate } = require('./lib/settlement-gate.js');
const { fiveFieldExtra } = require('./lib/five-field-extra.js');
+const { mfrGate } = require('./lib/mfr-gate.js');
+const { reusedMfrSet, stagingColorFor } = require('./lib/mfr-gate-resolve.js');
const { validateBeforeActivate } = require(path.join(os.homedir(),'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
const { execSync } = require('child_process');
const TOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1];
@@ -49,13 +51,15 @@ async function loadPubs(){ const r=await gql(`{publications(first:50){edges{node
async function setHold(id,verdict,reason){ await gql(`mutation($mfs:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mfs){userErrors{message}}}`,{mfs:[{ownerId:id,namespace:'internal',key:'settlement_hold',type:'single_line_text_field',value:`${verdict}:${reason}`.slice(0,250)}]}); }
// ---- MDC set from staging (authoritative). PID -> {mfr,material,width,dw_sku,vital}
-const rows=execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT shopify_product_id,mfr_sku,coalesce(material,''),coalesce(width,''),dw_sku,(private_label_collection='Dublin' AND pattern_name ~* 'vital') FROM mdc_catalog WHERE shopify_product_id IS NOT NULL ORDER BY dw_sku;"`).toString().trim().split('\n');
-const SET=rows.map(l=>{const[pid,mfr,mat,width,dw,vital]=l.split('\t');return{pid,mfr,mat,width,dw,vital:vital==='t'};}).filter(r=>r.pid);
+const rows=execSync(`psql "host=/tmp dbname=dw_unified" -tAF '\t' -c "SELECT shopify_product_id,mfr_sku,coalesce(material,''),coalesce(width,''),dw_sku,(private_label_collection='Dublin' AND pattern_name ~* 'vital'),coalesce(color_name,'') FROM mdc_catalog WHERE shopify_product_id IS NOT NULL ORDER BY dw_sku;"`).toString().trim().split('\n');
+const SET=rows.map(l=>{const[pid,mfr,mat,width,dw,vital,color]=l.split('\t');return{pid,mfr,mat,width,dw,vital:vital==='t',color};}).filter(r=>r.pid);
+// MFR-PROVENANCE gate (TK-11063) — load the cross-vendor reused-code set once.
+const REUSED_MFRS=reusedMfrSet();
(async()=>{
const gate=new SettlementGate(); P(`settlement lock: ${JSON.stringify(gate.lockStatus())} mode=${DRY?'DRY':'COMMIT'} mdc_set=${SET.length} allowVital=${ALLOW_VITAL}`);
const pubs=DRY?[]:await loadPubs(); if(!DRY)P(`publications (ex-Google): ${pubs.map(p=>p.name).join(', ')}`);
- let scanned=0,activated=0,publishedN=0,heldSettle=0,heldValidate=0,blocked=0,heldLeak=0,heldVital=0,cost=0;
+ let scanned=0,activated=0,publishedN=0,heldSettle=0,heldValidate=0,blocked=0,heldLeak=0,heldVital=0,heldMfr=0,cost=0;
const todo=SET.slice(0,LIMIT===Infinity?SET.length:LIMIT);
for(const row of todo){
scanned++;
@@ -70,6 +74,12 @@ const SET=rows.map(l=>{const[pid,mfr,mat,width,dw,vital]=l.split('\t');return{pi
if(HARD_LEAK.test(surface)){ heldLeak++; P(`HOLD(leak) ${row.dw} ${row.mfr}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Hold-Leak-Review']}); continue; }
// (0b) Dublin "Vital" generic-word hold — needs human OK before customer-facing
if(row.vital && !ALLOW_VITAL){ heldVital++; P(`HOLD(vital-review) ${row.dw} ${n.title}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Hold-Review']}); continue; }
+ // (0c) MFR-PROVENANCE gate (TK-11063) — BLOCK blank/reused/fabricated mfr codes before customer-facing.
+ // Resolve mfr from the live metafield first (custom.manufacturer_sku), fall back to the staging mfr_sku;
+ // staging color_name from mdc_catalog is the fabricated-placeholder signal (mdc_catalog isn't vendor_registry-mapped).
+ const mfrCode=(mfVal(mfs,'dwc','manufacturer_sku')||mfVal(mfs,'custom','manufacturer_sku')||mfVal(mfs,'global','manufacturer_sku')||row.mfr||'').trim();
+ const mg=mfrGate({vendor:'Phillipe Romano',mfr:mfrCode,reusedAcrossVendors:!!mfrCode&&REUSED_MFRS.has(mfrCode),stagingColorName:row.color||null});
+ if(!mg.ok){ heldMfr++; P(`HOLD(mfr:${mg.reasons.join(',')}) ${row.dw} ${mfrCode||'(blank)'}`); if(!DRY) await gql(`mutation($id:ID!,$t:[String!]!){tagsAdd(id:$id,tags:$t){userErrors{message}}}`,{id:n.id,t:['Needs-MfrSKU']}); continue; }
// (1) 5-field / specs gate (real validator)
const val=validateBeforeActivate({title:n.title,dwSku:row.dw,vendor:'Phillipe Romano',tags:n.tags||[],
descriptionHtml:n.descriptionHtml,variants:n.variants.nodes,
@@ -97,5 +107,5 @@ const SET=rows.map(l=>{const[pid,mfr,mat,width,dw,vital]=l.split('\t');return{pi
if(activated%25===0)P(` ...activated ${activated}`);
await sleep(300);
}
- P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} ${DRY?'wouldActivate':'activated'}=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate} heldLeak=${heldLeak} heldVital=${heldVital} settlement_cost=$${cost.toFixed(4)}`);
+ P(`\nDONE ${DRY?'(DRY)':''}: scanned=${scanned} ${DRY?'wouldActivate':'activated'}=${activated} published=${publishedN} heldSettlement=${heldSettle} blocked=${blocked} heldValidate=${heldValidate} heldLeak=${heldLeak} heldVital=${heldVital} heldMfr=${heldMfr} settlement_cost=$${cost.toFixed(4)}`);
})();
diff --git a/rotate-activate.js b/rotate-activate.js
index 9b41ac8..e9102af 100644
--- a/rotate-activate.js
+++ b/rotate-activate.js
@@ -44,6 +44,8 @@ const { execFileSync } = require('child_process');
const { ROTATION_ORDER_SQL } = require('./lib/rotation-order.js');
const { SettlementGate } = require('./lib/settlement-gate.js');
const { fiveFieldExtra } = require('./lib/five-field-extra.js');
+const { mfrGate } = require('./lib/mfr-gate.js');
+const { reusedMfrSet, stagingColorFor } = require('./lib/mfr-gate-resolve.js');
const { validateBeforeActivate, toImageList } =
require(path.join(os.homedir(), 'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
@@ -172,6 +174,14 @@ function materialFromNode(n) {
const mfs = n.metafields?.nodes || [];
return mfVal(mfs, 'global', 'material') || mfVal(mfs, 'custom', 'material') || mfVal(mfs, 'specs', 'material') || '';
}
+// Resolve the manufacturer SKU the same way the go-live-gate canary does: prefer the
+// metafield (dwc/custom/global.manufacturer_sku), fall back to nothing (the mirror's
+// mfr_sku column isn't on the live node — the metafield IS the live truth).
+function resolvedMfrFromNode(n) {
+ const mfs = n.metafields?.nodes || [];
+ return (mfVal(mfs, 'dwc', 'manufacturer_sku') || mfVal(mfs, 'custom', 'manufacturer_sku') ||
+ mfVal(mfs, 'global', 'manufacturer_sku') || '').trim();
+}
function pidToGid(shopify_id) {
// shopify_id already looks like gid://shopify/Product/NNN in this table.
@@ -249,6 +259,14 @@ function leakGuard(title, vendor) {
// SETTLEMENT GATE — construct once (runs lock.sh fail-closed). Plain textures
// auto-pass at $0; motif/print/botanical products are Gemini-vision-gated.
+ // MFR-PROVENANCE GATE — load the cross-vendor reused-code set ONCE (the internal-
+ // counter bug class), then per product resolve the live mfr metafield + its staging
+ // color name and BLOCK any DRAFT→ACTIVE whose mfr is blank / reused / fabricated
+ // (Carnegie "Grain 6300 → 63001…" placeholder-color class). TK-11063.
+ const reusedMfrs = reusedMfrSet();
+ console.log(`mfr-gate: ${reusedMfrs.size} mfr code(s) reused across >1 vendor (internal-counter watch)`);
+ let mfrBlocked = 0;
+
const settlement = new SettlementGate();
const ls = settlement.lockStatus();
console.log(`settlement gate: lock=${ls.ok ? 'PASS' : 'FAIL (' + ls.msg + ')'} — ` +
@@ -293,17 +311,32 @@ function leakGuard(title, vendor) {
const gate = gateFromLive(n, q && q.dw_sku, q && q.vendor);
const extra = fiveFieldExtra(n);
const leak = leakGuard(n.title, q && q.vendor);
- const passes = gate.ok && extra.ok && leak.ok;
+ // MFR-PROVENANCE gate (TK-11063). Resolve the live mfr metafield, its cross-vendor
+ // reuse flag, and its staging color name, then run the pure gate. Staging lookup is
+ // only worth doing when the code is otherwise present + not reused (the fabricated
+ // class is unique-and-present), so we short-circuit the DB read on blank/reused.
+ const mfrCode = resolvedMfrFromNode(n);
+ const reused = !!mfrCode && reusedMfrs.has(mfrCode);
+ const stagingColor = (mfrCode && !reused) ? stagingColorFor(q && q.vendor, mfrCode) : null;
+ const mfr = mfrGate({ vendor: q && q.vendor, mfr: mfrCode,
+ reusedAcrossVendors: reused, stagingColorName: stagingColor });
+ const passes = gate.ok && extra.ok && leak.ok && mfr.ok;
+ if (!mfr.ok) mfrBlocked++;
const rec = { ts: new Date().toISOString(), shopify_id: n.id, vendor: q && q.vendor,
dw_sku: q && q.dw_sku, mat_tier: q && q.mat_tier, rr: q && q.rr,
- title: n.title, passes,
+ title: n.title, passes, mfr_code: mfrCode,
reasons: [...(gate.ok ? [] : gate.reasons), ...(extra.ok ? [] : extra.reasons),
- ...(leak.ok ? [] : [leak.reason])] };
+ ...(leak.ok ? [] : [leak.reason]), ...(mfr.ok ? [] : mfr.reasons)] };
if (!passes) {
skipped++;
// NEVER activate a broken product — log + skip. It flows through the
- // field-fix drain and is re-tried on the next rotation pass.
+ // field-fix drain and is re-tried on the next rotation pass. If the ONLY
+ // failure is the mfr-provenance gate, tag Needs-MfrSKU so the drain/human
+ // can restore the real code (COMMIT mode only; the tag itself is reversible).
+ if (!DRY && !mfr.ok && gate.ok && extra.ok && leak.ok) {
+ try { await gqlRetry(TAGS_ADD, { id: n.id, tags: ['Needs-MfrSKU'] }); } catch (_) {}
+ }
fs.appendFileSync(AUDIT, JSON.stringify({ ...rec, action: 'skip-gate' }) + '\n');
continue;
}
@@ -363,6 +396,7 @@ function leakGuard(title, vendor) {
const sstat = settlement.stats();
console.log(`\n=== ROTATION ACTIVATOR ${DRY ? 'DRY-RUN' : 'RUN'} DONE ===`);
console.log(`scanned=${scanned} ${DRY ? 'would-activate' : 'activated'}=${activated} published=${published} skipped(gate-fail)=${skipped} alreadyActive=${alreadyActive}`);
+ console.log(`mfr-gate: blocked ${mfrBlocked} product(s) for blank/reused/fabricated mfr provenance`);
console.log(`settlement: auto-pass-textures=${sstat.autoPassCount} vision-calls=${sstat.visionCalls} blocked=${settlementBlocked} held=${settlementHeld}`);
console.log(`settlement cost: textures $0 (local) + vision ${sstat.visionCalls} calls = $${sstat.visionCostTotal.toFixed(5)} (Gemini 2.5-flash; ~$0.0006/img)`);
if (!DRY) console.log(`daily activation ledger: ${ledgerUsed()}/${DAILY_ACTIVATION_CAP}`);
diff --git a/test/mfr-gate.test.js b/test/mfr-gate.test.js
new file mode 100644
index 0000000..badff02
--- /dev/null
+++ b/test/mfr-gate.test.js
@@ -0,0 +1,69 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { mfrGate } = require('../lib/mfr-gate.js');
+
+// KNOWN-GOOD: a real Carnegie code with a real color name in staging → PASS.
+test('passes a real mfr code with a real staging color name', () => {
+ assert.deepEqual(
+ mfrGate({ vendor: 'Carnegie', mfr: '24121-windows', reusedAcrossVendors: false, stagingColorName: 'Oatmeal' }),
+ { ok: true, reasons: [] });
+});
+
+// A real code with no staging row consulted still PASSes (fail-open on the fabricated dim).
+test('passes a present code when no staging row is available', () => {
+ assert.deepEqual(
+ mfrGate({ vendor: 'Thibaut', mfr: 'T10958', reusedAcrossVendors: false, stagingColorName: null }),
+ { ok: true, reasons: [] });
+});
+
+// KNOWN-FABRICATED: the Carnegie "Grain 6300 → 63002" member with a placeholder "Color 2".
+test('BLOCKS the Carnegie fabricated <base>+<seq> code (placeholder color)', () => {
+ const r = mfrGate({ vendor: 'Carnegie', mfr: '63002', reusedAcrossVendors: false, stagingColorName: 'Color 2' });
+ assert.equal(r.ok, false);
+ assert.ok(r.reasons.includes('mfr-sku-fabricated-placeholder-color'));
+});
+
+test('placeholder color regex tolerates casing / spacing / British spelling', () => {
+ for (const c of ['Color 1', 'color 10', 'COLOR 12', 'Colour 3', 'colour10']) {
+ const r = mfrGate({ vendor: 'Carnegie', mfr: '6300' + c.replace(/\D/g, ''), reusedAcrossVendors: false, stagingColorName: c });
+ assert.equal(r.ok, false, `${c} should be treated as placeholder`);
+ }
+});
+
+// (a) blank / null / 'unknown' → BLOCK.
+test('BLOCKS a blank / null / unknown mfr code', () => {
+ for (const mfr of ['', ' ', null, undefined, 'Unknown', 'UNKNOWN']) {
+ const r = mfrGate({ vendor: 'Elitis', mfr, reusedAcrossVendors: false, stagingColorName: 'Chartreuse' });
+ assert.equal(r.ok, false, `${String(mfr)} should block`);
+ assert.ok(r.reasons.includes('mfr-sku-blank'));
+ }
+});
+
+// (b) reused across vendors → BLOCK (internal-counter bug class).
+test('BLOCKS a code reused across >1 vendor', () => {
+ const r = mfrGate({ vendor: 'IKSEL', mfr: '800010', reusedAcrossVendors: true, stagingColorName: 'Ivory' });
+ assert.equal(r.ok, false);
+ assert.ok(r.reasons.includes('mfr-sku-reused-across-vendors'));
+});
+
+// by-colour exempt line (Novasuede) is never a mfr FAIL even with a blank code.
+test('exempts by-colour lines (Novasuede) from the blank-code block', () => {
+ assert.deepEqual(
+ mfrGate({ vendor: 'Novasuede', mfr: '', reusedAcrossVendors: false, stagingColorName: null }),
+ { ok: true, reasons: [] });
+});
+
+// a real color name on a present unique code → PASS even for Carnegie.
+test('passes a Carnegie code whose staging color is a real name', () => {
+ assert.deepEqual(
+ mfrGate({ vendor: 'Carnegie', mfr: '62901-upholstery', reusedAcrossVendors: false, stagingColorName: 'Espresso' }),
+ { ok: true, reasons: [] });
+});
+
+// defensive: garbage ctx never throws, fails closed on a blank.
+test('handles a non-object ctx without throwing (fails closed)', () => {
+ const r = mfrGate(null);
+ assert.equal(r.ok, false);
+ assert.ok(r.reasons.includes('mfr-sku-blank'));
+});
← d9e84e1 rotation-activator: block $4.25 sample-leak price from passi
·
back to Dw Activation Debug TK11314
·
TK-11186 Step D prep: rotation-activator skips showroom vend 37bb28a →