[object Object]

← back to York Reprice 2026 08

TK-11547: weight go-live gate on the york publish cadence

2827a4cc0e12d588fde8e7d89926980e6d4b3662 · 2026-09-12 08:11:30 -0700 · Steve Abrams

publish-632.mjs flips York DRAFTs to ACTIVE on com.steve.york-publish-cadence. It already
held for settlement REVIEW but had no weight condition, so it could publish a product live
at zero weight — which collapses the order into the lowest weight tier / free-shipping band
and mis-costs DW freight (Steve's TK-11414 rule).

Found by shopify-activator-register (TK-11498) on its first run, not by hand — this file was
never in the TK-11414/TK-11471 field of view.

- The Sku query now selects inventoryItem measurement weight value+unit, and widens
  first:1 -> first:10. Without the select the gate would read a response that never carried
  weight and pass 100%; without the widen a zero-weight SAMPLE sibling would be invisible,
  and dw-active-weight-canary FAILs on those too (43 of the 83 live offenders were samples).
- HOLD, not self-heal, for two reasons: this script only ever flips status (weight belongs
  upstream at create, and a zero here means the upstream payload regressed and should be
  seen), and it authenticates with the NARROW SHOPIFY_ADMIN_TOKEN which lacks
  write_inventory — a heal would 403 and, if swallowed, publish at zero weight anyway.
- Held items are counted and named in the run summary, so this cannot become a silent stall.

test-weight-gate.mjs — 11/11, offline. Injected faults (zero sellable, zero SAMPLE, null,
absent measurement, negative, NaN) all HOLD; GRAMS/KILOGRAMS/OUNCES convert and do not
falsely hold; plus four structural checks that the gate cannot be vacuous — the query really
selects weight, it is no longer first:1, the hold precedes the mutation, and held items are
surfaced rather than silently dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 2827a4cc0e12d588fde8e7d89926980e6d4b3662
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 12 08:11:30 2026 -0700

    TK-11547: weight go-live gate on the york publish cadence
    
    publish-632.mjs flips York DRAFTs to ACTIVE on com.steve.york-publish-cadence. It already
    held for settlement REVIEW but had no weight condition, so it could publish a product live
    at zero weight — which collapses the order into the lowest weight tier / free-shipping band
    and mis-costs DW freight (Steve's TK-11414 rule).
    
    Found by shopify-activator-register (TK-11498) on its first run, not by hand — this file was
    never in the TK-11414/TK-11471 field of view.
    
    - The Sku query now selects inventoryItem measurement weight value+unit, and widens
      first:1 -> first:10. Without the select the gate would read a response that never carried
      weight and pass 100%; without the widen a zero-weight SAMPLE sibling would be invisible,
      and dw-active-weight-canary FAILs on those too (43 of the 83 live offenders were samples).
    - HOLD, not self-heal, for two reasons: this script only ever flips status (weight belongs
      upstream at create, and a zero here means the upstream payload regressed and should be
      seen), and it authenticates with the NARROW SHOPIFY_ADMIN_TOKEN which lacks
      write_inventory — a heal would 403 and, if swallowed, publish at zero weight anyway.
    - Held items are counted and named in the run summary, so this cannot become a silent stall.
    
    test-weight-gate.mjs — 11/11, offline. Injected faults (zero sellable, zero SAMPLE, null,
    absent measurement, negative, NaN) all HOLD; GRAMS/KILOGRAMS/OUNCES convert and do not
    falsely hold; plus four structural checks that the gate cannot be vacuous — the query really
    selects weight, it is no longer first:1, the hold precedes the mutation, and held items are
    surfaced rather than silently dropped.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 publish-632.mjs      | 26 ++++++++++++++++++++++++--
 test-weight-gate.mjs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/publish-632.mjs b/publish-632.mjs
index 68bbbcf..4fa884c 100644
--- a/publish-632.mjs
+++ b/publish-632.mjs
@@ -17,15 +17,37 @@ const toPublish=pay.filter(p=>!hold.has(norm(p.mfr_sku)));
 console.log(`publish-632 — ${APPLY?'⚠️ LIVE PUBLISH (DRAFT→ACTIVE)':'DRY-RUN'}`);
 console.log(`  total ${pay.length} | HELD (settlement REVIEW): ${pay.length-toPublish.length} | eligible: ${toPublish.length} | cap this run: ${LIMIT===Infinity?'ALL':LIMIT}`);
 if(!APPLY){console.log('  held mfr_skus:',[...hold].join(', '));console.log('\nDRY-RUN. Apply: node publish-632.mjs --apply --i-am-steve');process.exit(0);}
-const Sku=`query($q:String!){productVariants(first:1,query:$q){nodes{product{id status}}}}`;
+// TK-11547 (TK-11414 rule): the query MUST select weight, or the gate below reads a response that
+// never carried it, measures nothing, and passes 100% — the false-green class. first:1 -> first:10
+// so EVERY variant is seen: dw-active-weight-canary FAILs on a zero-weight SAMPLE too.
+const Sku=`query($q:String!){productVariants(first:10,query:$q){nodes{sku product{id status variants(first:10){nodes{sku title price inventoryItem{measurement{weight{value unit}}}}}}}}}`;
 const Mup=`mutation($input:ProductInput!){productUpdate(input:$input){product{status} userErrors{message}}}`;
-let done=0,skip=0,miss=0,err=0;
+let done=0,skip=0,miss=0,err=0,heldW=0; const heldWskus=[];
 for(const p of toPublish){
   const sku=p.variants[1].sku;
   const d=await gql(Sku,{q:`sku:${sku}`}); const prod=d.productVariants.nodes[0]?.product;
   if(!prod){miss++;continue;} if(prod.status==='ACTIVE'){skip++;continue;}
+  // TK-11547 WEIGHT GO-LIVE GATE (Steve's TK-11414 rule): never publish a product ACTIVE 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 it.
+  // HOLD, not self-heal: this script only ever flips status, and it authenticates with the NARROW
+  // SHOPIFY_ADMIN_TOKEN, which lacks write_inventory — a heal would fail 403 and, if that failure
+  // were swallowed, publish at zero weight anyway. Holding is the only honest option here. The
+  // weight belongs upstream at create; a zero reaching this point means the upstream payload
+  // regressed and should be seen, not silently patched. Held items are COUNTED and reported, so
+  // this cannot become a silent stall.
+  const zw=(prod.variants?.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);
+  });
+  if(!(prod.variants?.nodes||[]).length){ heldW++; heldWskus.push(`${sku}(no-variants-returned)`); continue; }
+  if(zw.length){ heldW++; heldWskus.push(`${sku}(${zw.map(v=>v.sku||v.title||'?').join('/')})`); continue; }
   try{const u=await gql(Mup,{input:{id:prod.id,status:'ACTIVE'}});(u.productUpdate?.userErrors||[]).length?err++:done++;}catch(e){err++;}
   if((done+skip+err)%50===0)console.log(`  progress published=${done} skip=${skip} err=${err}`);
   if(done>=LIMIT){console.log(`  cap reached: ${done} published this run (--limit=${LIMIT})`);break;}
 }
 console.log(`\nDONE — published ${done} ACTIVE, ${skip} already-active, ${miss} not-found, ${err} err. (${pay.length-toPublish.length} held DRAFT for settlement review)`);
+if(heldW)console.log(`  ⏸ HELD ${heldW} for weight>0 (TK-11547): ${heldWskus.slice(0,15).join(', ')}${heldWskus.length>15?` (+${heldWskus.length-15} more)`:''}`);
diff --git a/test-weight-gate.mjs b/test-weight-gate.mjs
new file mode 100644
index 0000000..6c84fb7
--- /dev/null
+++ b/test-weight-gate.mjs
@@ -0,0 +1,48 @@
+// test-weight-gate.mjs — TK-11547 negative test for the weight gate in publish-632.mjs.
+// Offline: zero network, zero Shopify. A positive-only test on a gate proves nothing
+// (CLAUDE.md TK-11431 amendment 3), so every case is an injected fault that must HOLD.
+import fs from 'node:fs';
+import assert from 'node:assert';
+const SRC = fs.readFileSync(new URL('./publish-632.mjs', import.meta.url), 'utf8');
+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++; } };
+
+// The gate predicate, lifted verbatim from publish-632.mjs.
+const zeroOf = (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 V = (sku, lb, unit = 'POUNDS') => ({ sku, title: sku, inventoryItem: { measurement: { weight: lb == null ? null : { value: lb, unit } } } });
+
+t('STRUCTURAL: the Sku query selects weight (else the gate measures nothing)', () => {
+  const q = SRC.slice(SRC.indexOf('const Sku='), SRC.indexOf('const Mup='));
+  assert.ok(/measurement\s*\{\s*weight\s*\{\s*value\s+unit/.test(q), 'Sku query does not select measurement weight value+unit');
+});
+t('STRUCTURAL: query widened past first:1 so every variant is seen', () => {
+  const q = SRC.slice(SRC.indexOf('const Sku='), SRC.indexOf('const Mup='));
+  assert.ok(!/productVariants\(first:1,/.test(q), 'still first:1 — a zero-weight sibling variant would be invisible');
+});
+t('STRUCTURAL: the hold precedes the productUpdate call', () => {
+  assert.ok(SRC.indexOf('heldW++') < SRC.indexOf('await gql(Mup'), 'gate does not precede the ACTIVATE mutation');
+});
+t('STRUCTURAL: held items are counted AND reported (a silent stall is not acceptable)', () => {
+  assert.ok(/heldWskus/.test(SRC) && /HELD \$\{heldW\}/.test(SRC), 'held count is not surfaced in the summary');
+});
+
+t('zero-weight sellable => HELD', () => assert.strictEqual(zeroOf([V('DWYK-1', 0), V('DWYK-1-Sample', 0.25)]).length, 1));
+t('zero-weight SAMPLE => HELD (samples count; the canary FAILs on them)', () => assert.strictEqual(zeroOf([V('DWYK-1', 3), V('DWYK-1-Sample', 0)]).length, 1));
+t('null weight => HELD', () => assert.strictEqual(zeroOf([V('DWYK-1', null)]).length, 1));
+t('missing measurement entirely => HELD, not silently OK', () => assert.strictEqual(zeroOf([{ sku: 'x', inventoryItem: {} }]).length, 1));
+t('negative / NaN => HELD', () => { assert.strictEqual(zeroOf([V('a', -1)]).length, 1); assert.strictEqual(zeroOf([V('a', 'zz')]).length, 1); });
+t('fully weighted => PUBLISHES (gate is not trigger-happy)', () => assert.strictEqual(zeroOf([V('DWYK-1', 3), V('DWYK-1-Sample', 0.25)]).length, 0));
+t('GRAMS / KILOGRAMS / OUNCES convert and do NOT falsely hold', () => {
+  assert.strictEqual(zeroOf([V('a', 1360, 'GRAMS')]).length, 0);
+  assert.strictEqual(zeroOf([V('a', 1.36, 'KILOGRAMS')]).length, 0);
+  assert.strictEqual(zeroOf([V('a', 4, 'OUNCES')]).length, 0);
+});
+
+console.log(`\n${fail ? 'TESTS FAILED' : 'ALL TESTS PASS'} — ${pass} passed, ${fail} failed`);
+process.exit(fail ? 1 : 0);

← 9e6b918 auto-data-snapshot: 2026-08-25T12:44:18 (1 data files) — __p  ·  back to York Reprice 2026 08  ·  auto-data-snapshot: 2026-09-15T01:48:50 (1 data files) — cad b9f39ac →