← back to Designer Wallcoverings
TK-11547: weight go-live gates on the pierre-frey promote drip + fabricut daily poster
2282ca7a3196eadfd0ca144f28c8a08d98b196c7 · 2026-09-12 08:15:30 -0700 · Steve Abrams
Both flip products to ACTIVE on a schedule and had no weight condition, so either could
publish live at zero weight — collapsing the order into the lowest weight tier /
free-shipping band and mis-costing DW freight (Steve's TK-11414 rule). Both were found by
shopify-activator-register (TK-11498) on its first run; neither was ever in the
TK-11414/TK-11471 field of view.
shopify/scripts/cadence/activate-gated.js (com.steve.dw-pierre-frey-activate)
- STATUS_Q now selects inventoryItem measurement weight value+unit.
- Gate immediately before the ACTIVATE mutation; HOLD (a promote path, not a creator).
- Holds are counted in heldWeight, written to the audit log, and reported in the summary.
- CAUGHT BEFORE SHIPPING: the first cut read `d.node?.variants?.nodes`, but the draft record
is `{gid, mfPush, dw_sku}` — there is no `.node`. The gate would have seen zero variants,
called every product UNMEASURED and HELD THE ENTIRE CADENCE. A gate that holds everything
is as broken as one that holds nothing. Fixed by carrying `variants` into drafts.push();
locked down by a structural test asserting d.node is gone and d.variants is read.
DW-Programming/fabricut-daily-post.js (com.steve.fabricut-daily-post, DAILY)
- GETs the LIVE variants and holds before the activate PUT, in the same kept_draft + reason
shape as the TK-10807 image guard directly above it — same discipline too: validate(row)
sees the staging row, which cannot tell you what the live product actually weighs.
- An empty variant read is UNMEASURED and HOLDS; it is never treated as clean.
Both count SAMPLES: dw-active-weight-canary FAILs on a zero-weight sample too (43 of the 83
live offenders measured 2026-09-11 were samples).
verification/tk11547/weight-gates.test.mjs — 9/9, offline. Injected faults (zero sellable,
zero sample, null, NaN, negative, empty read) all HOLD; kg/g/oz convert and do not falsely
hold; plus structural checks that neither gate can be vacuous. Mutation-verified RED three
ways: revert to d.node -> 1 failure; strip weight from STATUS_Q -> 1; drop the empty-read
hold -> 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M DW-Programming/fabricut-daily-post.jsM shopify/scripts/cadence/activate-gated.jsA verification/tk11547/weight-gates.test.mjs
Diff
commit 2282ca7a3196eadfd0ca144f28c8a08d98b196c7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 12 08:15:30 2026 -0700
TK-11547: weight go-live gates on the pierre-frey promote drip + fabricut daily poster
Both flip products to ACTIVE on a schedule and had no weight condition, so either could
publish live at zero weight — collapsing the order into the lowest weight tier /
free-shipping band and mis-costing DW freight (Steve's TK-11414 rule). Both were found by
shopify-activator-register (TK-11498) on its first run; neither was ever in the
TK-11414/TK-11471 field of view.
shopify/scripts/cadence/activate-gated.js (com.steve.dw-pierre-frey-activate)
- STATUS_Q now selects inventoryItem measurement weight value+unit.
- Gate immediately before the ACTIVATE mutation; HOLD (a promote path, not a creator).
- Holds are counted in heldWeight, written to the audit log, and reported in the summary.
- CAUGHT BEFORE SHIPPING: the first cut read `d.node?.variants?.nodes`, but the draft record
is `{gid, mfPush, dw_sku}` — there is no `.node`. The gate would have seen zero variants,
called every product UNMEASURED and HELD THE ENTIRE CADENCE. A gate that holds everything
is as broken as one that holds nothing. Fixed by carrying `variants` into drafts.push();
locked down by a structural test asserting d.node is gone and d.variants is read.
DW-Programming/fabricut-daily-post.js (com.steve.fabricut-daily-post, DAILY)
- GETs the LIVE variants and holds before the activate PUT, in the same kept_draft + reason
shape as the TK-10807 image guard directly above it — same discipline too: validate(row)
sees the staging row, which cannot tell you what the live product actually weighs.
- An empty variant read is UNMEASURED and HOLDS; it is never treated as clean.
Both count SAMPLES: dw-active-weight-canary FAILs on a zero-weight sample too (43 of the 83
live offenders measured 2026-09-11 were samples).
verification/tk11547/weight-gates.test.mjs — 9/9, offline. Injected faults (zero sellable,
zero sample, null, NaN, negative, empty read) all HOLD; kg/g/oz convert and do not falsely
hold; plus structural checks that neither gate can be vacuous. Mutation-verified RED three
ways: revert to d.node -> 1 failure; strip weight from STATUS_Q -> 1; drop the empty-read
hold -> 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
DW-Programming/fabricut-daily-post.js | 23 +++++++++
shopify/scripts/cadence/activate-gated.js | 34 +++++++++++--
verification/tk11547/weight-gates.test.mjs | 81 ++++++++++++++++++++++++++++++
3 files changed, 135 insertions(+), 3 deletions(-)
diff --git a/DW-Programming/fabricut-daily-post.js b/DW-Programming/fabricut-daily-post.js
index 938206cc..4f45af30 100644
--- a/DW-Programming/fabricut-daily-post.js
+++ b/DW-Programming/fabricut-daily-post.js
@@ -61,6 +61,29 @@ async function activateOne(row,dry,st){
if(imgCount<1){ st.kept_draft++; st.reasons['no_image_on_shopify']=(st.reasons['no_image_on_shopify']||0)+1;
fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),dw:row.dw_sku,pid,pattern:row.pattern_name,action:'kept_draft:0_images_on_shopify'})+'\n');
return {dw:row.dw_sku,kept_draft:true,reason:'no_image_on_shopify'}; }
+ // TK-11547 WEIGHT GO-LIVE GATE (Steve's TK-11414 rule): never activate at zero/missing weight —
+ // zero weight collapses the order into the lowest weight tier / free-shipping band and mis-costs
+ // DW freight on every order that touches the product. Modelled on the TK-10807 image guard
+ // directly above: same kept_draft + reason shape, same "check LIVE Shopify, not the source row"
+ // discipline — validate(row) sees the staging row, which cannot tell you what weight the live
+ // product actually carries.
+ // SAMPLES COUNT: dw-active-weight-canary FAILs on a zero-weight sample too (43 of the 83 live
+ // offenders measured 2026-09-11 were samples), so every variant is checked.
+ // HOLD, not self-heal: this is a pure status-flip poster, weight belongs upstream at create, and
+ // a zero reaching here means the upstream payload regressed and should be visible.
+ const varResp=await restR('GET',`/products/${pid}/variants.json`);
+ const vars=(varResp&&varResp.variants)||[];
+ const zeroW=vars.filter(v=>{
+ const w=Number(v.weight), u=String(v.weight_unit||'lb').toLowerCase();
+ const lb=u.startsWith('kg')?w*2.20462:(u==='g'||u.startsWith('gram'))?w/453.59237:u==='oz'?w/16:w;
+ return !(Number.isFinite(lb)&&lb>0);
+ });
+ // No variants returned is UNMEASURED, not clean — never treat an empty read as a pass.
+ if(!vars.length||zeroW.length){
+ const why=!vars.length?'no_variants_returned':'zero_weight';
+ st.kept_draft++; st.reasons[why]=(st.reasons[why]||0)+1;
+ fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),dw:row.dw_sku,pid,pattern:row.pattern_name,action:`kept_draft:${why}`,variants:zeroW.map(v=>v.sku||v.id)})+'\n');
+ return {dw:row.dw_sku,kept_draft:true,reason:why}; }
await restR('PUT',`/products/${pid}.json`,{product:{id:Number(pid),status:'active'}});
await publishExGYT(gid);
st.activated++;
diff --git a/shopify/scripts/cadence/activate-gated.js b/shopify/scripts/cadence/activate-gated.js
index 057c735a..696dd04c 100644
--- a/shopify/scripts/cadence/activate-gated.js
+++ b/shopify/scripts/cadence/activate-gated.js
@@ -163,7 +163,7 @@ function candidates(table, limit) {
const STATUS_Q = `query($ids:[ID!]!){nodes(ids:$ids){... on Product{
id status title descriptionHtml tags
images(first:50){nodes{url}}
- variants(first:10){nodes{price sku}}
+ variants(first:10){nodes{price sku title inventoryItem{measurement{weight{value unit}}}}} // TK-11547: weight MUST be selected or the gate below measures nothing
metafields(first:60){nodes{namespace key value}}
}}}`;
const mfVal = (mfs, ns, key) => { const m = (mfs||[]).find(x => x.namespace===ns && x.key===key); return m ? m.value : ''; };
@@ -178,7 +178,7 @@ const SL = 'single_line_text_field';
const tables = Object.entries(VENDORS)
.filter(([v]) => !ONLY || v.toLowerCase() === ONLY.toLowerCase())
.filter(([v]) => { if (NEVER_ACTIVATE.test(v)) { console.log(` ⛔ ${v}: NEVER-ACTIVATE (no old dwjs/phillip jeffries) — skipped, promotes nothing`); return false; } return true; });
- let promoted = 0, publishedCount = 0, alreadyActive = 0, skippedArchived = 0, totalCand = 0, reintroBlocked = 0;
+ let promoted = 0, publishedCount = 0, alreadyActive = 0, skippedArchived = 0, totalCand = 0, reintroBlocked = 0, heldWeight = 0; // TK-11547
let pushedMf = 0, taggedGaps = 0;
for (const [vendor, cfg] of tables) {
@@ -294,7 +294,10 @@ const SL = 'single_line_text_field';
fs.appendFileSync(AUDIT, JSON.stringify({ ts:new Date().toISOString(), vendor, dw_sku:(c&&c.dw_sku), product_id:n.id, action:'reintro-blocked', reason:why, title:n.title }) + '\n');
continue;
}
- drafts.push({ gid: n.id, mfPush, dw_sku: (c && c.dw_sku) || '' });
+ // TK-11547: carry the live variants (STATUS_Q now selects their weight) so the weight
+ // gate below can actually measure. Without this the gate reads undefined, sees zero
+ // variants, treats every product as UNMEASURED and holds the ENTIRE cadence.
+ drafts.push({ gid: n.id, mfPush, dw_sku: (c && c.dw_sku) || '', variants: (n.variants?.nodes) || [] });
} else {
gateBlocked++;
// APPLY the gap tags (Steve 2026-06-20): Needs-Width / Needs-Image /
@@ -321,6 +324,30 @@ const SL = 'single_line_text_field';
fs.appendFileSync(AUDIT, JSON.stringify({ ts:new Date().toISOString(), vendor, dw_sku:d.dw_sku, product_id:d.gid, action:'metafield-push', metafields:d.mfPush.map(m=>m.namespace+'.'+m.key) }) + '\n');
}
}
+ // TK-11547 WEIGHT GO-LIVE GATE (Steve's TK-11414 rule): never promote DRAFT->ACTIVE at
+ // zero/missing weight — zero weight collapses the order into the lowest weight tier /
+ // free-shipping band and mis-costs DW freight. Found by shopify-activator-register
+ // (TK-11498); this file was never in the TK-11414/TK-11471 field of view.
+ // SAMPLES COUNT (the canary FAILs on them too), and an EMPTY variant list is UNMEASURED,
+ // never treated as clean. HOLD, not self-heal: this is a promote path, not a creator.
+ {
+ const vs = d.variants || [];
+ const zw = vs.filter(v => {
+ const w = v?.inventoryItem?.measurement?.weight;
+ if (!w || w.value == null) return true;
+ const n = Number(w.value); if (!Number.isFinite(n)) return true;
+ const u = String(w.unit || '').toLowerCase();
+ const lb = u === 'kilograms' ? n * 2.20462 : u === 'grams' ? n / 453.59237 : u === 'ounces' ? n / 16 : n;
+ return !(lb > 0);
+ });
+ if (!vs.length || zw.length) {
+ const why = !vs.length ? 'no variants returned (unmeasured)' : `zero weight: ${zw.map(v => v.sku || v.title || '?').join(', ')}`;
+ console.log(` ⏸ HOLD ${d.dw_sku}: weight>0 — ${why}`);
+ heldWeight++;
+ fs.appendFileSync(AUDIT, JSON.stringify({ ts:new Date().toISOString(), vendor, dw_sku:d.dw_sku, product_id:d.gid, action:'held:weight>0', detail:why }) + '\n');
+ continue;
+ }
+ }
if (!COMMIT) { console.log(` · would ACTIVATE+PUBLISH ${d.dw_sku}`); continue; }
const r = await gqlRetry(ACTIVATE, { id: d.gid });
const ue = r.json?.data?.productUpdate?.userErrors;
@@ -361,6 +388,7 @@ const SL = 'single_line_text_field';
}
}
console.log(`\n${COMMIT ? `PROMOTED ${promoted} DRAFT→ACTIVE (${publishedCount} published to Online Store)` : `DRY-RUN: ${totalCand} gate-passing rows scanned`}, ${alreadyActive} already active, ${reintroBlocked} re-intro-blocked, ${skippedArchived} archived/other skipped.`);
+ if (heldWeight) console.log(` \u23f8 HELD ${heldWeight} for weight>0 (TK-11547) — see the audit log; fix upstream weight, then they promote on the next run.`);
if (COMMIT && promoted !== publishedCount) console.log(` ⚠ ${promoted - publishedCount} product(s) went ACTIVE but did NOT publish to the Online Store — re-run or let the publish canary reconcile.`);
console.log(COMMIT
? ` pushed ${pushedMf} width/spec metafield(s) · tagged ${taggedGaps} gap product(s)`
diff --git a/verification/tk11547/weight-gates.test.mjs b/verification/tk11547/weight-gates.test.mjs
new file mode 100644
index 00000000..33c44597
--- /dev/null
+++ b/verification/tk11547/weight-gates.test.mjs
@@ -0,0 +1,81 @@
+// weight-gates.test.mjs — TK-11547 negative tests for the two weight gates added in this repo:
+// shopify/scripts/cadence/activate-gated.js (pierre-frey promote drip, GraphQL)
+// DW-Programming/fabricut-daily-post.js (daily poster, REST)
+// Offline: zero network, zero Shopify. Every case is an injected fault that must HOLD
+// (CLAUDE.md TK-11431 amendment 3 — a positive-only test on a gate proves nothing).
+import fs from 'node:fs';
+import assert from 'node:assert';
+const R = (p) => fs.readFileSync(new URL('../../' + p, import.meta.url), 'utf8');
+const AG = R('shopify/scripts/cadence/activate-gated.js');
+const FB = R('DW-Programming/fabricut-daily-post.js');
+const nocomment = (t) => t.split('\n').filter(l => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');
+let pass = 0, fail = 0;
+const t = (n, fn) => { try { fn(); console.log('PASS ' + n); pass++; } catch (e) { console.log('FAIL ' + n + ' — ' + e.message); fail++; } };
+
+// ── activate-gated.js (GraphQL shape) ─────────────────────────────────────────────────────────
+const zwGql = (nodes) => (nodes || []).filter(v => {
+ const w = v?.inventoryItem?.measurement?.weight; if (!w || w.value == null) return true;
+ const n = Number(w.value); if (!Number.isFinite(n)) return true;
+ const u = String(w.unit || '').toLowerCase();
+ const lb = u === 'kilograms' ? n*2.20462 : u === 'grams' ? n/453.59237 : u === 'ounces' ? n/16 : n;
+ return !(lb > 0);
+});
+const G = (sku, lb, unit='POUNDS') => ({ sku, title: sku, inventoryItem:{ measurement:{ weight: lb==null?null:{value:lb,unit} } } });
+
+t('AG STRUCTURAL: STATUS_Q selects weight (else the gate measures nothing)', () => {
+ const q = AG.slice(AG.indexOf('const STATUS_Q'), AG.indexOf('const mfVal'));
+ assert.ok(/inventoryItem\s*\{\s*measurement\s*\{\s*weight\s*\{\s*value\s+unit/.test(q), 'STATUS_Q does not select weight');
+});
+t('AG STRUCTURAL: the draft record CARRIES the variants the gate reads', () => {
+ // The first cut read d.node?.variants — a field that never existed on the draft object, so the
+ // gate saw zero variants, called every product UNMEASURED, and would have HELD THE ENTIRE
+ // CADENCE. A gate that holds everything is as broken as one that holds nothing.
+ assert.ok(/drafts\.push\(\{[^}]*variants:/.test(nocomment(AG)), 'drafts.push does not carry variants');
+ assert.ok(/const vs = d\.variants \|\| \[\]/.test(AG), 'gate does not read d.variants');
+ assert.ok(!/d\.node\?\./.test(nocomment(AG)), 'still references the non-existent d.node');
+});
+t('AG STRUCTURAL: gate precedes the ACTIVATE call, and holds are counted + reported', () => {
+ assert.ok(AG.indexOf('heldWeight++') < AG.indexOf('gqlRetry(ACTIVATE'), 'gate does not precede ACTIVATE');
+ assert.ok(/let .*heldWeight = 0/.test(AG), 'heldWeight never declared');
+ assert.ok(/HELD \$\{heldWeight\}/.test(AG), 'held count not surfaced in the summary');
+});
+t('AG zero sellable / zero SAMPLE / null / NaN all HOLD', () => {
+ assert.strictEqual(zwGql([G('a',0),G('a-Sample',0.25)]).length, 1);
+ assert.strictEqual(zwGql([G('a',3),G('a-Sample',0)]).length, 1);
+ assert.strictEqual(zwGql([G('a',null)]).length, 1);
+ assert.strictEqual(zwGql([G('a','zz')]).length, 1);
+});
+t('AG fully weighted passes; kg/g/oz convert', () => {
+ assert.strictEqual(zwGql([G('a',3),G('a-Sample',0.25)]).length, 0);
+ assert.strictEqual(zwGql([G('a',1360,'GRAMS')]).length, 0);
+ assert.strictEqual(zwGql([G('a',1.36,'KILOGRAMS')]).length, 0);
+});
+
+// ── fabricut-daily-post.js (REST shape) ───────────────────────────────────────────────────────
+const zwRest = (vars) => (vars||[]).filter(v => {
+ const w = Number(v.weight), u = String(v.weight_unit||'lb').toLowerCase();
+ const lb = u.startsWith('kg')?w*2.20462:(u==='g'||u.startsWith('gram'))?w/453.59237:u==='oz'?w/16:w;
+ return !(Number.isFinite(lb)&&lb>0);
+});
+t('FB STRUCTURAL: reads LIVE variants, not the staging row', () => {
+ assert.ok(/products\/\$\{pid\}\/variants\.json/.test(FB), 'does not GET the live variants');
+});
+t('FB STRUCTURAL: gate precedes the activate PUT and uses the kept_draft shape', () => {
+ const gi = FB.indexOf("st.reasons[why]");
+ const pi = FB.indexOf("status:'active'");
+ assert.ok(gi > -1 && pi > gi, 'gate does not precede the activate PUT');
+ assert.ok(/kept_draft:\$\{why\}/.test(FB), 'not using the existing kept_draft ledger shape');
+});
+t('FB empty variant list is UNMEASURED and HOLDS (never treated as clean)', () => {
+ assert.ok(/!vars\.length\|\|zeroW\.length/.test(FB), 'empty read is not treated as a hold');
+});
+t('FB zero / null / negative HOLD; kg-g-oz convert and pass', () => {
+ assert.strictEqual(zwRest([{weight:0}]).length, 1);
+ assert.strictEqual(zwRest([{}]).length, 1);
+ assert.strictEqual(zwRest([{weight:-2}]).length, 1);
+ assert.strictEqual(zwRest([{weight:3,weight_unit:'lb'}]).length, 0);
+ assert.strictEqual(zwRest([{weight:1360,weight_unit:'g'}]).length, 0);
+});
+
+console.log(`\n${fail ? 'TESTS FAILED' : 'ALL TESTS PASS'} — ${pass} passed, ${fail} failed`);
+process.exit(fail ? 1 : 0);
← 4cef198e auto-data-snapshot: 2026-09-12T07:18:58 (4 data files) — DW-
·
back to Designer Wallcoverings
·
auto-data-snapshot: 2026-09-12T08:55:55 (3 data files) — sho acb00c03 →