← back to Carnegie Reprice
TK-11547: weight guard on the carnegie rollback un-archive (heal, never block)
081afeac27ffca3a98b26fc41c8c25b800c2df66 · 2026-09-12 08:13:06 -0700 · Steve Abrams
archive-redirect.mjs's rollback path PUTs status:'active' to un-archive products
(com.steve.carnegie-rollout). Despite the filename it is a real activator, and it had no
weight handling — so a restored product could go live at zero weight, collapsing orders into
the lowest weight tier / free-shipping band (Steve's TK-11414 rule).
Found by shopify-activator-register (TK-11498) on its first run; never in the
TK-11414/TK-11471 field of view.
DELIBERATELY DIFFERENT from every other TK-11547 site, which HOLD. This is an UNDO path: it
restores products that were intentionally archived. A gate that can block a rollback is worse
than the defect it prevents — it strands you mid-incident with no way back. So this never
refuses to un-archive:
1. read each variant's current weight
2. fill the TK-11414-approved default on any zero (sample 0.25 lb, sellable 3.0 lb)
3. RE-VERIFY — the write's own 200 is not evidence the weight is set
4. un-archive REGARDLESS, and if any variant is still zero, say so LOUDLY on stderr with the
ids, leaving dw-active-weight-canary (daily) as the backstop
Silence is the only outcome ruled out. The heal goes through the REST variant field because
this script authenticates with the narrow SHOPIFY_ADMIN_TOKEN, which lacks write_inventory —
the GraphQL inventoryItemUpdate path would 403.
test-weight-guard.mjs — 10/10, offline. Unit cases for zero/null/negative/NaN and kg/g/oz
conversion, plus five structural checks: it re-verifies after writing, it un-archives even
when the heal fails, a failed heal is loud rather than silent, the heal uses the REST path the
narrow token can actually call, and the sample/sellable default split is applied.
(One test caught itself: the "no GraphQL path" assertion initially matched the word
inventoryItemUpdate inside this guard's own explanatory comment and failed on correct code.
Now strips comments before asserting — prose is not behaviour.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M archive-redirect.mjsA test-weight-guard.mjs
Diff
commit 081afeac27ffca3a98b26fc41c8c25b800c2df66
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 12 08:13:06 2026 -0700
TK-11547: weight guard on the carnegie rollback un-archive (heal, never block)
archive-redirect.mjs's rollback path PUTs status:'active' to un-archive products
(com.steve.carnegie-rollout). Despite the filename it is a real activator, and it had no
weight handling — so a restored product could go live at zero weight, collapsing orders into
the lowest weight tier / free-shipping band (Steve's TK-11414 rule).
Found by shopify-activator-register (TK-11498) on its first run; never in the
TK-11414/TK-11471 field of view.
DELIBERATELY DIFFERENT from every other TK-11547 site, which HOLD. This is an UNDO path: it
restores products that were intentionally archived. A gate that can block a rollback is worse
than the defect it prevents — it strands you mid-incident with no way back. So this never
refuses to un-archive:
1. read each variant's current weight
2. fill the TK-11414-approved default on any zero (sample 0.25 lb, sellable 3.0 lb)
3. RE-VERIFY — the write's own 200 is not evidence the weight is set
4. un-archive REGARDLESS, and if any variant is still zero, say so LOUDLY on stderr with the
ids, leaving dw-active-weight-canary (daily) as the backstop
Silence is the only outcome ruled out. The heal goes through the REST variant field because
this script authenticates with the narrow SHOPIFY_ADMIN_TOKEN, which lacks write_inventory —
the GraphQL inventoryItemUpdate path would 403.
test-weight-guard.mjs — 10/10, offline. Unit cases for zero/null/negative/NaN and kg/g/oz
conversion, plus five structural checks: it re-verifies after writing, it un-archives even
when the heal fails, a failed heal is loud rather than silent, the heal uses the REST path the
narrow token can actually call, and the sample/sellable default split is applied.
(One test caught itself: the "no GraphQL path" assertion initially matched the word
inventoryItemUpdate inside this guard's own explanatory comment and failed on correct code.
Now strips comments before asserting — prose is not behaviour.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
archive-redirect.mjs | 40 ++++++++++++++++++++++++++++++++++++-
test-weight-guard.mjs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+), 1 deletion(-)
diff --git a/archive-redirect.mjs b/archive-redirect.mjs
index 6665e59..90cfe21 100644
--- a/archive-redirect.mjs
+++ b/archive-redirect.mjs
@@ -52,7 +52,45 @@ if(MODE==='rollback'){
// 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); }
+ // TK-11547 WEIGHT GO-LIVE GUARD (Steve's TK-11414 rule: never ACTIVE at zero/missing weight —
+ // zero weight collapses an order into the lowest weight tier / free-shipping band and mis-costs
+ // freight). Found by shopify-activator-register (TK-11498); this file was never in the
+ // TK-11414/TK-11471 field of view.
+ //
+ // HEAL-THEN-PROCEED-LOUDLY, deliberately NOT hold. Every other site gated under TK-11547 HOLDS,
+ // but this is an UNDO path: it restores products that were intentionally archived. A gate that
+ // can block a rollback is worse than the defect it prevents — it strands you mid-incident with
+ // no way back. So this never refuses to un-archive. It fills the approved default first, and if
+ // that fails it un-archives ANYWAY and says so loudly, leaving dw-active-weight-canary (daily)
+ // as the backstop. Silence is the only outcome ruled out.
+ //
+ // Heal goes through the REST variant field because this script authenticates with the NARROW
+ // SHOPIFY_ADMIN_TOKEN, which lacks write_inventory (the GraphQL inventoryItemUpdate path 403s).
+ const W_SAMPLE_LB = 0.25, W_DEFAULT_LB = 3.0; // TK-11414-approved defaults
+ const isSample = v => /sample|memo/i.test(String(v.title||v.option1||'')) || /-sample$/i.test(String(v.sku||''))
+ || Math.abs(Number(v.price)-4.25) < 0.01;
+ let healed=0, unhealed=[];
+ for(const a of map.archived){
+ try{
+ const pr = await (await shop(`/products/${a.old_id}.json`)).json();
+ for(const v of (pr.product?.variants||[])){
+ const w = Number(v.weight);
+ const 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;
+ if(Number.isFinite(lb) && lb > 0) continue;
+ const target = isSample(v) ? W_SAMPLE_LB : W_DEFAULT_LB;
+ await shop(`/variants/${v.id}.json`,{method:'PUT',body:JSON.stringify({variant:{id:v.id,weight:target,weight_unit:'lb'}})});
+ await sleep(400);
+ // RE-VERIFY: the mutation's own 200 is not evidence the weight is set.
+ const re = await (await shop(`/variants/${v.id}.json`)).json();
+ const rw = Number(re.variant?.weight);
+ if(Number.isFinite(rw) && rw > 0) healed++; else unhealed.push(`${a.old_id}/${v.sku||v.id}`);
+ }
+ }catch(e){ unhealed.push(`${a.old_id}(read/heal error: ${e.message})`); }
+ await shop(`/products/${a.old_id}.json`,{method:'PUT',body:JSON.stringify({product:{id:a.old_id,status:'active'}})}); await sleep(550);
+ }
+ if(healed) console.log(`[rollback] TK-11547: healed ${healed} zero-weight variant(s) to the approved default before un-archiving`);
+ if(unhealed.length) console.error(`[rollback] ⚠️ TK-11547: ${unhealed.length} variant(s) are LIVE AT ZERO WEIGHT — un-archived anyway (an undo must never be blocked). dw-active-weight-canary will flag these within 24h; fix with scripts/tk11471-weight-backfill.py. ${unhealed.slice(0,15).join(', ')}`);
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/test-weight-guard.mjs b/test-weight-guard.mjs
new file mode 100644
index 0000000..b0c0048
--- /dev/null
+++ b/test-weight-guard.mjs
@@ -0,0 +1,55 @@
+// test-weight-guard.mjs — TK-11547 test for the rollback weight guard in archive-redirect.mjs.
+// Offline. The design here is deliberately DIFFERENT from every other TK-11547 site (heal-then-
+// proceed-loudly rather than HOLD, because blocking an undo is worse than the defect), so the
+// tests assert BOTH halves: that it heals, and that it still un-archives when healing fails.
+import fs from 'node:fs';
+import assert from 'node:assert';
+const SRC = fs.readFileSync(new URL('./archive-redirect.mjs', import.meta.url), 'utf8');
+const BLOCK = SRC.slice(SRC.indexOf('TK-11547 WEIGHT GO-LIVE GUARD'), SRC.indexOf('for(const r of map.redirects)'));
+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 zero-detection predicate, lifted verbatim
+const zero = (v) => { const w = Number(v.weight); const 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('detects zero weight', () => assert.strictEqual(zero({ weight: 0 }), true));
+t('detects null/absent weight', () => { assert.strictEqual(zero({}), true); assert.strictEqual(zero({ weight: null }), true); });
+t('detects negative / NaN', () => { assert.strictEqual(zero({ weight: -1 }), true); assert.strictEqual(zero({ weight: 'x' }), true); });
+t('accepts a positive lb weight', () => assert.strictEqual(zero({ weight: 3, weight_unit: 'lb' }), false));
+t('kg / g / oz convert and are not falsely flagged', () => {
+ assert.strictEqual(zero({ weight: 1.36, weight_unit: 'kg' }), false);
+ assert.strictEqual(zero({ weight: 1360, weight_unit: 'g' }), false);
+ assert.strictEqual(zero({ weight: 4, weight_unit: 'oz' }), false);
+});
+
+t('STRUCTURAL: the guard RE-VERIFIES after writing (a 200 is not evidence)', () => {
+ assert.ok(/RE-VERIFY/.test(BLOCK) && /re\.variant\?\.weight/.test(BLOCK), 'no re-read after the heal write');
+});
+t('STRUCTURAL: it un-archives EVEN WHEN the heal fails — an undo is never blocked', () => {
+ const healIdx = BLOCK.indexOf('unhealed.push');
+ const unarchIdx = BLOCK.indexOf("status:'active'");
+ assert.ok(healIdx > -1 && unarchIdx > healIdx, 'the un-archive does not follow the failed-heal path');
+ assert.ok(!/continue;\s*\}\s*$/m.test(BLOCK.slice(healIdx, unarchIdx)), 'a failed heal appears to skip the un-archive');
+});
+t('STRUCTURAL: a failed heal is LOUD (console.error), never silent', () => {
+ assert.ok(/console\.error\(/.test(BLOCK) && /LIVE AT ZERO WEIGHT/.test(BLOCK), 'unhealed variants are not surfaced loudly');
+});
+t('STRUCTURAL: heal uses the REST variant field (narrow token lacks write_inventory)', () => {
+ assert.ok(/\/variants\/\$\{v\.id\}\.json/.test(BLOCK), 'not using the REST variant path');
+ // Strip comment lines before asserting absence. The first cut matched the word
+ // `inventoryItemUpdate` inside this guard's OWN explanatory comment (which says that path 403s
+ // on the narrow token) and failed on correct code — an assertion that reads prose as if it were
+ // behaviour. Worth locking down: it is the same imprecision that made an earlier structural
+ // check vacuously true by comparing against a constant's declaration instead of its call site.
+ const CODE = BLOCK.split('\n').filter(l => !/^\s*\/\//.test(l)).join('\n');
+ assert.ok(!/inventoryItemUpdate/.test(CODE), 'uses a GraphQL path the narrow token cannot call');
+});
+t('STRUCTURAL: samples get the sample default, sellables the type default', () => {
+ assert.ok(/W_SAMPLE_LB = 0\.25/.test(BLOCK) && /W_DEFAULT_LB = 3\.0/.test(BLOCK), 'approved defaults missing');
+ assert.ok(/isSample\(v\) \? W_SAMPLE_LB : W_DEFAULT_LB/.test(BLOCK), 'sample/sellable split not applied');
+});
+
+console.log(`\n${fail ? 'TESTS FAILED' : 'ALL TESTS PASS'} — ${pass} passed, ${fail} failed`);
+process.exit(fail ? 1 : 0);
← 1634913 auto-data-snapshot: 2026-09-11T10:08:53 (3 data files) — rep
·
back to Carnegie Reprice
·
carnegie phase2: retry metafield writes, record failures, gu 5f484b7 →