[object Object]

← back to Designerwallcoverings

TK-11471: weight-guard matches the canary invariant (samples count) + tests

61d93588e0dafb38cd1d627c04c3a7bd1829d966 · 2026-09-11 11:25:37 -0700 · Steve Abrams

The gate and dw-active-weight-canary disagreed about the invariant. The canary FAILs on ANY
zero-weight ACTIVE variant (its last live run split the offenders 195 sample / 190 sellable),
but zeroWeightBlockers() filtered samples OUT — so a product with a zero-weight SAMPLE passed
the gate and then turned the canary red.

- add allZeroWeightVariants(product): EVERY zero-weight variant, sample included
- zeroWeightBlockers() kept for back-compat, marked @deprecated
- add healAndVerifyWeights(gql, gid, product): SELF-HEAL then RE-VERIFY (the pattern Steve
  approved in sanderson-onboard create_sdg.mjs 8d09eed), failing CLOSED on an unreadable
  re-query, an unhealable variant, a throw, or a userError
- add WEIGHT_REQUERY / M_WEIGHT_SET so a call site cannot ship a query that measures nothing
- fix currentWeightLb: GRAMS/OUNCES on the GraphQL path were read as POUNDS (1360 g -> 1360 lb)

Tests (offline, no network/DB): scripts/lib/weight-guard.test.mjs (51 assertions) and
verification/tk11471/go-live-gate-replay.test.mjs (24). Both carry NEGATIVE tests that go red
on an injected fault per CLAUDE.md TK-11431 amendment 3 — the replay harness caught a real
false-green in the first cut of healAndVerifyWeights (an unhealed variant passing on a clean
re-verify), which is why it now fails closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJHxAzaEMMxado57mFjiCk

Files touched

Diff

commit 61d93588e0dafb38cd1d627c04c3a7bd1829d966
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 11:25:37 2026 -0700

    TK-11471: weight-guard matches the canary invariant (samples count) + tests
    
    The gate and dw-active-weight-canary disagreed about the invariant. The canary FAILs on ANY
    zero-weight ACTIVE variant (its last live run split the offenders 195 sample / 190 sellable),
    but zeroWeightBlockers() filtered samples OUT — so a product with a zero-weight SAMPLE passed
    the gate and then turned the canary red.
    
    - add allZeroWeightVariants(product): EVERY zero-weight variant, sample included
    - zeroWeightBlockers() kept for back-compat, marked @deprecated
    - add healAndVerifyWeights(gql, gid, product): SELF-HEAL then RE-VERIFY (the pattern Steve
      approved in sanderson-onboard create_sdg.mjs 8d09eed), failing CLOSED on an unreadable
      re-query, an unhealable variant, a throw, or a userError
    - add WEIGHT_REQUERY / M_WEIGHT_SET so a call site cannot ship a query that measures nothing
    - fix currentWeightLb: GRAMS/OUNCES on the GraphQL path were read as POUNDS (1360 g -> 1360 lb)
    
    Tests (offline, no network/DB): scripts/lib/weight-guard.test.mjs (51 assertions) and
    verification/tk11471/go-live-gate-replay.test.mjs (24). Both carry NEGATIVE tests that go red
    on an injected fault per CLAUDE.md TK-11431 amendment 3 — the replay harness caught a real
    false-green in the first cut of healAndVerifyWeights (an unhealed variant passing on a clean
    re-verify), which is why it now fails closed.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01FJHxAzaEMMxado57mFjiCk
---
 scripts/lib/weight-guard.mjs                      |  99 ++++++++++-
 scripts/lib/weight-guard.test.mjs                 | 206 ++++++++++++++++++++++
 verification/tk11471/go-live-gate-replay.test.mjs | 131 ++++++++++++++
 3 files changed, 430 insertions(+), 6 deletions(-)

diff --git a/scripts/lib/weight-guard.mjs b/scripts/lib/weight-guard.mjs
index 67d600e..8d8313d 100644
--- a/scripts/lib/weight-guard.mjs
+++ b/scripts/lib/weight-guard.mjs
@@ -4,7 +4,15 @@
 // mis-costs DW freight). Mirrors the inventory-stamp-guard.mjs pattern: a pure,
 // side-effect-free module onboarders import at two call sites —
 //   1. create payload:  weight: resolveWeightLb(variant, product)  (unit POUNDS)
-//   2. before activate:  const b = zeroWeightBlockers(product); if (b.length) don't flip ACTIVE
+//   2. before activate:  const r = await healAndVerifyWeights(gql, gid, product);
+//                       if (!r.ok) HOLD as draft (never flip ACTIVE at zero weight)
+//
+// TK-11471 (2026-09-11) — the gate and the canary DISAGREED about the invariant:
+// dw-active-weight-canary FAILs on ANY zero-weight ACTIVE variant (its last live run split the
+// offenders 195 sample / 190 sellable — i.e. SAMPLES COUNT), but zeroWeightBlockers() filtered
+// samples OUT. A product with a zero-weight SAMPLE therefore passed the gate and then turned the
+// canary red. allZeroWeightVariants() is the gate that matches the canary; zeroWeightBlockers()
+// is kept only for back-compat and is DEPRECATED.
 //
 // Defaults come straight from the approved TK-11414 backfill (samples 0.25 lb,
 // sellable per-product-type). Keep these in sync with that backfill.
@@ -41,7 +49,11 @@ export function currentWeightLb(variant = {}) {
   const gql = variant?.inventoryItem?.measurement?.weight;
   if (gql && gql.value != null) {
     const v = Number(gql.value);
-    return (norm(gql.unit) === 'kilograms') ? v * 2.20462 : v; // else assume POUNDS/GRAMS below
+    const u = norm(gql.unit);                       // Shopify WeightUnit enum
+    if (u === 'kilograms' || u.startsWith('kg')) return v * 2.20462;
+    if (u === 'grams' || u === 'g') return v / 453.59237;   // TK-11471: was read as POUNDS
+    if (u === 'ounces' || u === 'oz') return v / 16;        // TK-11471: was read as POUNDS
+    return v;                                       // POUNDS (and unit-less ⇒ assume lb)
   }
   if (variant.grams != null) return Number(variant.grams) / 453.59237;
   if (variant.weight != null) {
@@ -73,9 +85,84 @@ export function resolveWeightLb(variant = {}, product = {}) {
   return (Number.isFinite(w) && w > 0) ? w : defaultWeightLb(variant, product);
 }
 
-/** THE ACTIVATE-SIDE GUARD: sellable variants that would go live at zero weight.
- *  If this is non-empty, DO NOT flip the product to ACTIVE (Steve's rule). */
+/** Normalize a product's variants out of either a GraphQL connection or a plain array. */
+export function variantsOf(product = {}) {
+  return product.variants?.edges?.map(e => e.node) ?? product.variants ?? [];
+}
+
+/** @deprecated TK-11471 — SELLABLE-ONLY view; it filters samples OUT, so it does NOT match the
+ *  invariant dw-active-weight-canary enforces (that canary fails on ANY zero-weight ACTIVE
+ *  variant, samples included). Kept for back-compat with existing call sites.
+ *  Use allZeroWeightVariants() for any new gate. */
 export function zeroWeightBlockers(product = {}) {
-  const variants = product.variants?.edges?.map(e => e.node) ?? product.variants ?? [];
-  return variants.filter(v => !isSampleVariant(v) && hasZeroWeight(v));
+  return variantsOf(product).filter(v => !isSampleVariant(v) && hasZeroWeight(v));
+}
+
+/** THE ACTIVATE-SIDE GUARD (TK-11471): EVERY zero-weight variant, sample included.
+ *  Matches dw-active-weight-canary exactly. Non-empty ⇒ DO NOT flip the product ACTIVE. */
+export function allZeroWeightVariants(product = {}) {
+  return variantsOf(product).filter(v => hasZeroWeight(v));
+}
+
+/** The re-query a go-live site must run so the guard MEASURES something. A product query that
+ *  omits inventoryItem{measurement{weight}} makes every variant look zero-weight to
+ *  currentWeightLb — and a query that omits productType silently defaults every heal to
+ *  FALLBACK_LB. Both fields are required. */
+export const WEIGHT_REQUERY = `query($id:ID!){ product(id:$id){ productType variants(first:100){edges{node{ id sku title price inventoryItem{ id measurement{ weight{ value unit } } } }}} } }`;
+
+export const M_WEIGHT_SET = `mutation($id:ID!,$w:Float!){ inventoryItemUpdate(id:$id, input:{measurement:{weight:{value:$w, unit:POUNDS}}}){ userErrors{message} } }`;
+
+/**
+ * SELF-HEAL then VERIFY, the pattern Steve approved in sanderson-onboard/scripts/create_sdg.mjs
+ * (8d09eed) — stranding product is worse than assigning the already-approved default, but a heal
+ * that silently fails must NEVER activate.
+ *
+ *   1. every zero-weight variant (sample included) is written defaultWeightLb() in POUNDS
+ *   2. the product is RE-READ and re-checked — the mutation's own 200 is not evidence
+ *   3. ok === false  ⇒ caller must HOLD the product as draft and name `weight>0`
+ *
+ * Idempotent + no-op when all weights are already positive (zero network calls in that case).
+ * Fails CLOSED: an unreadable re-query, a missing inventoryItem id, or a userError all yield
+ * ok:false rather than a silent pass.
+ *
+ * @param {(q:string,v:object)=>Promise<any>} gql  the call site's own gql(query, variables)
+ * @param {string} productGid                      gid://shopify/Product/<id>
+ * @param {object} product                         the already-fetched product (weights + productType)
+ */
+export async function healAndVerifyWeights(gql, productGid, product = {}, opts = {}) {
+  const requery = opts.requery || WEIGHT_REQUERY;
+  const mutation = opts.mutation || M_WEIGHT_SET;
+  const errs = [], healed = [];
+  let healFailures = 0;                 // TK-11471: an UNHEALED variant is never a PASS
+  const productType = product.productType || product.product_type;
+
+  const zero = allZeroWeightVariants(product);
+  if (!zero.length) return { ok: true, healed, stillZero: [], errs };   // no-op
+
+  for (const v of zero) {
+    const iid = v?.inventoryItem?.id;
+    const label = v.sku || v.title || v.id || '?';
+    if (!iid) { healFailures++; errs.push(`weight:no-inventory-item:${label}`); continue; }
+    const lb = defaultWeightLb(v, { productType });
+    let r;
+    try { r = await gql(mutation, { id: iid, w: lb }); }
+    catch (e) { healFailures++; errs.push(`weight:${label}:${String(e && e.message || e).slice(0, 80)}`); continue; }
+    const ue = r?.inventoryItemUpdate?.userErrors || [];
+    if (ue.length) healFailures++;
+    ue.forEach(e => errs.push(`weight:${label}:${e.message}`));
+    healed.push({ sku: label, inventoryItemId: iid, lb });
+  }
+
+  // RE-VERIFY against the live record. Never trust the write.
+  let fresh;
+  try { fresh = (await gql(requery, { id: productGid }))?.product; }
+  catch (e) { errs.push(`weight:reverify:${String(e && e.message || e).slice(0, 80)}`); }
+  if (!fresh) { errs.push('weight:reverify-failed'); return { ok: false, healed, stillZero: [], errs }; }
+
+  const stillZero = allZeroWeightVariants(fresh).map(v => v.sku || v.title || v.id || '?');
+  // FAIL CLOSED on an UNHEALED variant even when the re-verify comes back clean. A variant we
+  // could not write (no inventoryItem id, a throw, a userError) is UNMEASURED with respect to our
+  // own action; a clean re-verify that happens to disagree is not licence to activate. Holding is
+  // reversible and the next run is a no-op, so the conservative branch costs nothing.
+  return { ok: stillZero.length === 0 && healFailures === 0, healed, stillZero, errs };
 }
diff --git a/scripts/lib/weight-guard.test.mjs b/scripts/lib/weight-guard.test.mjs
new file mode 100644
index 0000000..c037c95
--- /dev/null
+++ b/scripts/lib/weight-guard.test.mjs
@@ -0,0 +1,206 @@
+#!/usr/bin/env node
+/**
+ * weight-guard.test.mjs — TK-11471. Pure, offline, zero-network, zero-DB.
+ *   node scripts/lib/weight-guard.test.mjs
+ *
+ * CLAUDE.md TK-11431 amendment 3: a check ships with a NEGATIVE test proving it goes red on an
+ * injected fault, or it does not ship. Every assertion below that matters injects the fault
+ * (a zero weight, a null measurement, a failed heal) and proves the guard FLAGS it — a
+ * positive-only suite on a detector proves nothing.
+ */
+import {
+  allZeroWeightVariants, zeroWeightBlockers, currentWeightLb, hasZeroWeight,
+  resolveWeightLb, defaultWeightLb, isSampleVariant, healAndVerifyWeights,
+  SAMPLE_WEIGHT_LB, FALLBACK_LB, TYPE_DEFAULT_LB, M_WEIGHT_SET, WEIGHT_REQUERY,
+} from './weight-guard.mjs';
+
+let pass = 0, fail = 0;
+const ok = (name, cond, detail = '') => {
+  if (cond) { pass++; console.log(`  ok   ${name}`); }
+  else { fail++; console.error(`  FAIL ${name}${detail ? ' — ' + detail : ''}`); }
+};
+const eq = (name, got, want) => ok(name, Object.is(got, want), `got ${JSON.stringify(got)} want ${JSON.stringify(want)}`);
+const near = (name, got, want, tol = 1e-4) => ok(name, Math.abs(got - want) < tol, `got ${got} want ~${want}`);
+const section = t => console.log(`\n── ${t}`);
+
+// ── fixture builders ────────────────────────────────────────────────────────────
+const gqlVariant = (sku, value, unit = 'POUNDS', extra = {}) => ({
+  id: `gid://shopify/ProductVariant/${sku}`, sku, title: extra.title ?? 'Roll',
+  price: extra.price ?? '199.00',
+  inventoryItem: { id: `gid://shopify/InventoryItem/${sku}`,
+    measurement: value === null ? null : { weight: value === undefined ? null : { value, unit } } },
+});
+const product = (variants, productType = 'Wallcovering') =>
+  ({ productType, variants: { edges: variants.map(node => ({ node })) } });
+
+// ════════════════════════════════════════════════════════════════════════════════
+section('allZeroWeightVariants — the invariant dw-active-weight-canary enforces');
+
+// NEGATIVE TEST 1: injected zero-weight SELLABLE variant must go red.
+{
+  const p = product([gqlVariant('DWTT-1001', 0), gqlVariant('DWTT-1001-Sample', 0.25, 'POUNDS', { title: 'Sample', price: '4.25' })]);
+  const flagged = allZeroWeightVariants(p);
+  eq('zero-weight SELLABLE flagged', flagged.length, 1);
+  eq('  …and it is the sellable one', flagged[0].sku, 'DWTT-1001');
+}
+
+// NEGATIVE TEST 2: injected zero-weight SAMPLE must go red. THIS IS THE TK-11471 BUG —
+// zeroWeightBlockers() filters samples out and passes, then dw-active-weight-canary turns red
+// (its last live run split the offenders 195 sample / 190 sellable, so samples DO count).
+{
+  const p = product([
+    gqlVariant('DWTT-1002', 3),
+    gqlVariant('DWTT-1002-Sample', 0, 'POUNDS', { title: 'Sample', price: '4.25' }),
+  ]);
+  const flagged = allZeroWeightVariants(p);
+  eq('zero-weight SAMPLE flagged by allZeroWeightVariants', flagged.length, 1);
+  eq('  …and it is the sample', flagged[0].sku, 'DWTT-1002-Sample');
+  // The regression this replaces — proves the OLD gate was blind to exactly this product:
+  eq('deprecated zeroWeightBlockers MISSES it (the bug)', zeroWeightBlockers(p).length, 0);
+}
+
+// NEGATIVE TEST 3: null / missing / absent measurement must FAIL SAFE (flagged, never weighted).
+{
+  const p = product([
+    gqlVariant('null-measurement', null),        // inventoryItem.measurement === null
+    gqlVariant('null-weight', undefined),        // measurement.weight === null
+    { id: 'v4', sku: 'no-inventoryItem', title: 'Roll', price: '10.00' },  // field absent entirely
+    { id: 'v5', sku: 'explicit-null-weight', weight: null, weight_unit: 'lb' },
+  ]);
+  eq('null/missing/absent measurement all flagged', allZeroWeightVariants(p).length, 4);
+  eq('currentWeightLb(null measurement) === 0', currentWeightLb(gqlVariant('x', null)), 0);
+  ok('hasZeroWeight({}) fails safe', hasZeroWeight({}));
+}
+
+// POSITIVE CONTROL: an all-weighted product must be empty (and the detector must not cry wolf).
+{
+  const p = product([gqlVariant('DWTT-1003', 3), gqlVariant('DWTT-1003-Sample', 0.25, 'POUNDS', { title: 'Sample', price: '4.25' })]);
+  eq('all-weighted product => empty result', allZeroWeightVariants(p).length, 0);
+}
+
+section('unit conversion');
+{
+  near('kilograms converts to lb', currentWeightLb(gqlVariant('kg', 1.36, 'KILOGRAMS')), 2.99828);
+  ok('positive KG weight is NOT flagged', !hasZeroWeight(gqlVariant('kg', 1.36, 'KILOGRAMS')));
+  eq('a 0-value KG weight IS flagged', allZeroWeightVariants(product([gqlVariant('kg0', 0, 'KILOGRAMS')])).length, 1);
+  // TK-11471 fix: GRAMS/OUNCES used to fall through and be read as POUNDS (1360 g -> "1360 lb").
+  near('grams converts to lb', currentWeightLb(gqlVariant('g', 1360, 'GRAMS')), 2.99828);
+  near('ounces converts to lb', currentWeightLb(gqlVariant('oz', 48, 'OUNCES')), 3);
+  near('REST variant.weight lb passthrough', currentWeightLb({ weight: 3, weight_unit: 'lb' }), 3);
+  near('REST variant.grams converts', currentWeightLb({ grams: 1360 }), 2.99828);
+}
+
+section('resolveWeightLb (create-side)');
+{
+  near('preserves an existing positive weight', resolveWeightLb({ weight: 7.5, weight_unit: 'lb' }, { productType: 'Wallcovering' }), 7.5);
+  near('preserves a positive GQL weight', resolveWeightLb(gqlVariant('p', 4.2), { productType: 'Wallcovering' }), 4.2);
+  near('fills the per-type default (Wallcovering=3)', resolveWeightLb({ sku: 'DWX-1', option1: 'Roll' }, { productType: 'Wallcovering' }), TYPE_DEFAULT_LB['Wallcovering']);
+  near('fills the per-type default (Mural=4)', resolveWeightLb({ sku: 'DWX-2', option1: 'Roll' }, { productType: 'Mural' }), TYPE_DEFAULT_LB['Mural']);
+  near('fills the per-type default (Fabric=1)', resolveWeightLb({ sku: 'DWX-3', option1: 'Yard' }, { productType: 'Fabric' }), TYPE_DEFAULT_LB['Fabric']);
+  near('unknown product_type => FALLBACK_LB', resolveWeightLb({ sku: 'DWX-4', option1: 'Roll' }, { productType: 'Nonesuch' }), FALLBACK_LB);
+  near('sample => SAMPLE_WEIGHT_LB', resolveWeightLb({ sku: 'DWX-5-Sample', option1: 'Sample' }, { productType: 'Wallcovering' }), SAMPLE_WEIGHT_LB);
+  near('$4.25 memo detected as sample', resolveWeightLb({ sku: 'DWX-6', option1: 'Memo', price: '4.25' }, { productType: 'Wallcovering' }), SAMPLE_WEIGHT_LB);
+  near('zero weight => default, not 0', resolveWeightLb({ weight: 0, weight_unit: 'lb', sku: 'DWX-7', option1: 'Roll' }, { productType: 'Wallcovering' }), 3);
+  ok('isSampleVariant(-Sample sku)', isSampleVariant({ sku: 'DWX-8-Sample' }));
+  ok('isSampleVariant(Roll) false', !isSampleVariant({ sku: 'DWX-8', title: 'Roll', price: '199.00' }));
+  near('defaultWeightLb honors product_type snake_case too', defaultWeightLb({ sku: 'r', option1: 'Roll' }, { product_type: 'Mural' }), 4);
+}
+
+// ════════════════════════════════════════════════════════════════════════════════
+section('healAndVerifyWeights — SELF-HEAL then VERIFY (offline mock gql)');
+
+/** Mock gql. `healOutcome` decides what the RE-QUERY returns, so we can inject a failed heal. */
+function mockGql({ before, after, failMutation = false, reverifyNull = false, throwOn = null }) {
+  const calls = [];
+  return {
+    calls,
+    gql: async (q, v) => {
+      calls.push({ q: q.includes('inventoryItemUpdate') ? 'MUTATION' : 'QUERY', v });
+      if (throwOn && q.includes(throwOn)) throw new Error('boom');
+      if (q.includes('inventoryItemUpdate')) {
+        return failMutation
+          ? { inventoryItemUpdate: { userErrors: [{ message: 'Access denied for inventoryItemUpdate' }] } }
+          : { inventoryItemUpdate: { userErrors: [] } };
+      }
+      return reverifyNull ? { product: null } : { product: after ?? before };
+    },
+  };
+}
+
+// (i) HAPPY: zero weights are healed, re-verify comes back clean, ok === true.
+{
+  const before = product([gqlVariant('DWTT-2001', 0), gqlVariant('DWTT-2001-Sample', 0, 'POUNDS', { title: 'Sample', price: '4.25' })]);
+  const after = product([gqlVariant('DWTT-2001', 3), gqlVariant('DWTT-2001-Sample', 0.25, 'POUNDS', { title: 'Sample', price: '4.25' })]);
+  const m = mockGql({ before, after });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/1', before);
+  ok('heal succeeds => ok', r.ok === true, JSON.stringify(r));
+  eq('  both variants healed', r.healed.length, 2);
+  near('  sellable healed to the Wallcovering default', r.healed.find(h => h.sku === 'DWTT-2001').lb, 3);
+  near('  sample healed to SAMPLE_WEIGHT_LB', r.healed.find(h => h.sku === 'DWTT-2001-Sample').lb, SAMPLE_WEIGHT_LB);
+  eq('  stillZero empty', r.stillZero.length, 0);
+  eq('  no errors', r.errs.length, 0);
+  eq('  2 mutations + 1 re-verify', m.calls.length, 3);
+  eq('  the last call is the RE-VERIFY query', m.calls[2].q, 'QUERY');
+}
+
+// (ii) NEGATIVE: the heal SILENTLY FAILS (write 200s, weight still 0) => ok MUST be false.
+{
+  const before = product([gqlVariant('DWTT-2002', 0)]);
+  const m = mockGql({ before, after: before });    // re-query shows it is STILL zero
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/2', before);
+  ok('heal silently failed => NOT ok', r.ok === false, JSON.stringify(r));
+  eq('  names the still-zero variant', r.stillZero[0], 'DWTT-2002');
+}
+
+// (iii) NEGATIVE: the mutation returns userErrors (no write_inventory scope) => NOT ok.
+{
+  const before = product([gqlVariant('DWTT-2003', 0)]);
+  const m = mockGql({ before, after: before, failMutation: true });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/3', before);
+  ok('mutation userErrors => NOT ok', r.ok === false);
+  ok('  the error is surfaced', r.errs.some(e => /Access denied/.test(e)), JSON.stringify(r.errs));
+}
+
+// (iv) NEGATIVE: re-verify unreadable (product null / __err) => FAIL CLOSED, never a silent pass.
+{
+  const before = product([gqlVariant('DWTT-2004', 0)]);
+  const m = mockGql({ before, reverifyNull: true });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/4', before);
+  ok('unreadable re-verify => NOT ok (fails closed)', r.ok === false);
+  ok('  says reverify-failed', r.errs.includes('weight:reverify-failed'));
+}
+
+// (v) NEGATIVE: a variant with no inventoryItem id cannot be healed => NOT ok.
+{
+  const before = product([{ id: 'v9', sku: 'DWTT-2005', title: 'Roll', price: '10.00' }]);
+  const m = mockGql({ before, after: before });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/5', before);
+  ok('no inventoryItem id => NOT ok', r.ok === false);
+  ok('  names the unhealable variant', r.errs.some(e => e.startsWith('weight:no-inventory-item:')));
+}
+
+// (vi) NEGATIVE: gql throws mid-heal => NOT ok (no unhandled rejection escapes).
+{
+  const before = product([gqlVariant('DWTT-2006', 0)]);
+  const m = mockGql({ before, after: before, throwOn: 'inventoryItemUpdate' });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/6', before);
+  ok('gql throw => NOT ok', r.ok === false);
+}
+
+// (vii) IDEMPOTENT NO-OP: already-weighted product makes ZERO network calls and returns ok.
+{
+  const before = product([gqlVariant('DWTT-2007', 3), gqlVariant('DWTT-2007-Sample', 0.25, 'POUNDS', { title: 'Sample', price: '4.25' })]);
+  const m = mockGql({ before });
+  const r = await healAndVerifyWeights(m.gql, 'gid://shopify/Product/7', before);
+  ok('already weighted => ok', r.ok === true);
+  eq('  zero gql calls (no-op)', m.calls.length, 0);
+}
+
+section('the shared query/mutation constants actually select what the guard measures');
+ok('WEIGHT_REQUERY selects measurement.weight', /measurement\s*\{\s*weight\s*\{\s*value\s+unit/.test(WEIGHT_REQUERY));
+ok('WEIGHT_REQUERY selects productType', /productType/.test(WEIGHT_REQUERY));
+ok('WEIGHT_REQUERY selects inventoryItem id', /inventoryItem\s*\{\s*id/.test(WEIGHT_REQUERY));
+ok('M_WEIGHT_SET writes POUNDS', /unit:\s*POUNDS/.test(M_WEIGHT_SET));
+
+console.log(`\n${fail === 0 ? 'PASS' : 'FAIL'} — ${pass} passed, ${fail} failed`);
+process.exit(fail === 0 ? 0 : 1);
diff --git a/verification/tk11471/go-live-gate-replay.test.mjs b/verification/tk11471/go-live-gate-replay.test.mjs
new file mode 100644
index 0000000..f47a1f2
--- /dev/null
+++ b/verification/tk11471/go-live-gate-replay.test.mjs
@@ -0,0 +1,131 @@
+#!/usr/bin/env node
+/**
+ * go-live-gate-replay.test.mjs — TK-11471. Offline mock-replay of the go-live WEIGHT GATE.
+ *   node verification/tk11471/go-live-gate-replay.test.mjs
+ *
+ * Zero network, zero DB, zero Shopify. A tiny in-memory fake store answers the SAME GraphQL
+ * documents the real onboarders send, and `replayGoLive()` below is a byte-faithful copy of the
+ * gate block inserted into all nine go-live sites — so this proves the GATE ITSELF, not just the
+ * library function underneath it.
+ *
+ * The two cases that matter (CLAUDE.md TK-11431 amendment 3 — break it on purpose, watch it go red):
+ *   (i)  a zero-weight product is HEALED and then ACTIVATED
+ *   (ii) a product whose heal FAILS is NOT activated — productUpdate(status:ACTIVE) is never sent
+ */
+import { healAndVerifyWeights, WEIGHT_REQUERY, M_WEIGHT_SET } from '../../scripts/lib/weight-guard.mjs';
+
+let pass = 0, fail = 0;
+const ok = (n, c, d = '') => { if (c) { pass++; console.log(`  ok   ${n}`); } else { fail++; console.error(`  FAIL ${n}${d ? ' — ' + d : ''}`); } };
+const eq = (n, g, w) => ok(n, Object.is(g, w), `got ${JSON.stringify(g)} want ${JSON.stringify(w)}`);
+
+// ── fake Shopify ────────────────────────────────────────────────────────────────
+// `writeIsBlackHole` injects the real-world failure we must survive: the mutation returns a clean
+// 200 with no userErrors, and the weight is NOT actually persisted (the silent-heal-failure class).
+function fakeStore({ productType = 'Wallcovering', variants, writeIsBlackHole = false }) {
+  const state = structuredClone(variants);
+  const sent = [];
+  const byItem = id => state.find(v => v.inventoryItem.id === id);
+  const shape = () => ({ productType, variants: { edges: state.map(node => ({ node: structuredClone(node) })) } });
+  const gql = async (q, v) => {
+    if (q.includes('inventoryItemUpdate') && q.includes('measurement')) {
+      sent.push({ op: 'WEIGHT', id: v.id, w: v.w });
+      if (!writeIsBlackHole) byItem(v.id).inventoryItem.measurement = { weight: { value: v.w, unit: 'POUNDS' } };
+      return { inventoryItemUpdate: { userErrors: [] } };
+    }
+    if (q.includes('status:ACTIVE')) {
+      sent.push({ op: 'ACTIVATE', id: v.id });
+      return { productUpdate: { product: { status: 'ACTIVE' }, userErrors: [] } };
+    }
+    sent.push({ op: 'QUERY' });
+    return { product: shape() };
+  };
+  return { gql, sent, shape, state };
+}
+const M_ACTIVE = `mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{status} userErrors{message} } }`;
+
+const V = (sku, lb, title = 'Roll', price = '199.00') => ({
+  id: `gid://shopify/ProductVariant/${sku}`, sku, title, price,
+  inventoryItem: { id: `gid://shopify/InventoryItem/${sku}`,
+    measurement: lb === null ? null : { weight: { value: lb, unit: 'POUNDS' } } },
+});
+
+/** VERBATIM copy of the gate block inserted into the nine go-live scripts. */
+async function replayGoLive(store, gidP) {
+  const errs = [];
+  const d = await store.gql(WEIGHT_REQUERY, { id: gidP });     // the site's own Q_VARIANTS, now selecting weight
+  // ── TK-11471 weight go-live gate ────────────────────────────────────────────
+  { const _w = await healAndVerifyWeights(store.gql, gidP, d.product);
+    _w.errs.forEach(e => errs.push(e));
+    if (!_w.ok) return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; }
+  // ────────────────────────────────────────────────────────────────────────────
+  const r4 = await store.gql(M_ACTIVE, { id: gidP });
+  (r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
+  return { status: r4.productUpdate?.product?.status, errs };
+}
+
+console.log('── (i) zero-weight product is HEALED then ACTIVATED');
+{
+  const s = fakeStore({ variants: [V('DWOS-3001', null), V('DWOS-3001-Sample', null, 'Sample', '4.25')] });
+  const res = await replayGoLive(s, 'gid://shopify/Product/3001');
+  eq('activated', res.status, 'ACTIVE');
+  ok('not held', !res.heldZeroWeight);
+  eq('no errors', res.errs.length, 0);
+  const weights = s.sent.filter(x => x.op === 'WEIGHT');
+  eq('both variants healed', weights.length, 2);
+  eq('sellable healed to 3 lb', weights.find(w => /3001$/.test(w.id)).w, 3);
+  eq('sample healed to 0.25 lb', weights.find(w => /Sample$/.test(w.id)).w, 0.25);
+  eq('ACTIVATE was sent', s.sent.filter(x => x.op === 'ACTIVATE').length, 1);
+  ok('ACTIVATE came AFTER the heal', s.sent.findIndex(x => x.op === 'ACTIVATE') > s.sent.findLastIndex(x => x.op === 'WEIGHT'));
+  ok('store really holds positive weights now', s.state.every(v => v.inventoryItem.measurement.weight.value > 0));
+}
+
+console.log('\n── (ii) INJECTED FAULT: the heal fails silently => product is NOT activated');
+{
+  const s = fakeStore({ variants: [V('DWOS-3002', null), V('DWOS-3002-Sample', null, 'Sample', '4.25')], writeIsBlackHole: true });
+  const res = await replayGoLive(s, 'gid://shopify/Product/3002');
+  ok('HELD', res.heldZeroWeight === true, JSON.stringify(res));
+  ok('names weight>0', res.errs.includes('heldZeroWeight: weight>0'));
+  eq('productUpdate(status:ACTIVE) NEVER sent', s.sent.filter(x => x.op === 'ACTIVATE').length, 0);
+  eq('no status returned', res.status, undefined);
+  ok('store still at zero weight', s.state.every(v => v.inventoryItem.measurement === null));
+}
+
+console.log('\n── (iii) already-weighted product: idempotent, no heal, still activates');
+{
+  const s = fakeStore({ variants: [V('DWOS-3003', 3), V('DWOS-3003-Sample', 0.25, 'Sample', '4.25')] });
+  const res = await replayGoLive(s, 'gid://shopify/Product/3003');
+  eq('activated', res.status, 'ACTIVE');
+  eq('zero weight-writes (no-op)', s.sent.filter(x => x.op === 'WEIGHT').length, 0);
+  eq('exactly 1 product read + 1 activate', s.sent.length, 2);
+}
+
+console.log('\n── (iv) INJECTED FAULT: ONLY the SAMPLE is zero-weight (the TK-11471 bug product)');
+{
+  // Under the old sellable-only gate this product sailed through and turned dw-active-weight-canary
+  // red. It must now heal the sample before activating.
+  const s = fakeStore({ variants: [V('DWOS-3004', 3), V('DWOS-3004-Sample', null, 'Sample', '4.25')] });
+  const res = await replayGoLive(s, 'gid://shopify/Product/3004');
+  eq('activated after healing the sample', res.status, 'ACTIVE');
+  const w = s.sent.filter(x => x.op === 'WEIGHT');
+  eq('exactly the sample was healed', w.length, 1);
+  ok('…and it was the sample', /Sample$/.test(w[0].id));
+  eq('healed to SAMPLE_WEIGHT_LB', w[0].w, 0.25);
+}
+
+console.log('\n── (v) INJECTED FAULT: a query that does NOT select weight must not silently pass');
+{
+  // The exact false-green class from CLAUDE.md: if a site's product query omits
+  // inventoryItem{measurement{weight}}, every variant reads as zero-weight. The guard must treat
+  // that as UNMEASURED-and-therefore-blocked (it heals, and if the write is a black hole, holds).
+  const blind = { productType: 'Wallcovering', variants: { edges: [
+    { node: { id: 'v1', sku: 'DWOS-3005', title: 'Roll', price: '199.00' } },   // no inventoryItem at all
+  ] } };
+  const s = fakeStore({ variants: [V('DWOS-3005', 3)] });
+  const r = await healAndVerifyWeights(s.gql, 'gid://shopify/Product/3005', blind);
+  ok('UNHEALED variant fails closed even when re-verify looks clean', r.ok === false, JSON.stringify(r));
+  ok('names the unhealable variant', r.errs.some(e => e.startsWith('weight:no-inventory-item:')));
+  eq('no weight mutation attempted on a variant with no inventory item', s.sent.filter(x => x.op === 'WEIGHT').length, 0);
+}
+
+console.log(`\n${fail === 0 ? 'PASS' : 'FAIL'} — ${pass} passed, ${fail} failed`);
+process.exit(fail === 0 ? 0 : 1);

← f518287 auto-data-snapshot: 2026-09-11T11:23:31 (2 data files) — ver  ·  back to Designerwallcoverings  ·  TK-11461: Sanderson gallery contamination root cause — input 8dc084a →