← back to Designerwallcoverings
TK-11471: weight go-live gate on the 9 unguarded ACTIVATE sites
6a9e28e2f8e0fc14f8b04ec2d1027649d0c9a905 · 2026-09-11 11:28:12 -0700 · Steve Abrams
artmura, fallingstar, knoll, maharam, muralsource, osborne, stout, stroheim, tres-tintas.
sanderson was already wired and is the reference.
Per site, immediately before productUpdate(status:ACTIVE):
- the product query now selects inventoryItem{id measurement{weight{value unit}}} and
productType. NONE of the nine selected weight before, so a gate dropped in without this
would have measured nothing and silently passed — the CLAUDE.md false-green class.
- healAndVerifyWeights() SELF-HEALS every zero-weight variant to the approved default, then
RE-VERIFIES. Still zero (or unhealable) => the product is HELD as draft with 'weight>0',
never activated. Idempotent: a fully weighted product makes zero extra API calls.
Each site keeps its own style (.js/.mjs, its own gql, its own result shape — fallingstar
returns the errs array its caller reads, the rest return { heldZeroWeight, errs }). No
inventory, price or publication logic touched; the TK-11357 /bin/zsh guard is untouched.
verification/tk11471/golive-wiring.test.mjs asserts all ten sites are wired AND carries five
negative tests (drop the import / drop measurement.weight / drop productType / delete the gate /
move it after the mutation) that each prove the detector goes red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJHxAzaEMMxado57mFjiCk
Files touched
M scripts/artmura-onboard/go-live-artmura.jsM scripts/fallingstar-onboard/go-live-fallingstar.jsM scripts/knoll-onboard/go-live.mjsM scripts/maharam-onboard/go-live.mjsM scripts/muralsource-onboard/go-live.mjsM scripts/osborne-onboard/go-live.mjsM scripts/stout-onboard/go-live.mjsM scripts/stroheim-onboard/go-live.mjsM scripts/tres-tintas-onboard/go-live-tres-tintas.jsA verification/tk11471/golive-wiring.test.mjs
Diff
commit 6a9e28e2f8e0fc14f8b04ec2d1027649d0c9a905
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 11:28:12 2026 -0700
TK-11471: weight go-live gate on the 9 unguarded ACTIVATE sites
artmura, fallingstar, knoll, maharam, muralsource, osborne, stout, stroheim, tres-tintas.
sanderson was already wired and is the reference.
Per site, immediately before productUpdate(status:ACTIVE):
- the product query now selects inventoryItem{id measurement{weight{value unit}}} and
productType. NONE of the nine selected weight before, so a gate dropped in without this
would have measured nothing and silently passed — the CLAUDE.md false-green class.
- healAndVerifyWeights() SELF-HEALS every zero-weight variant to the approved default, then
RE-VERIFIES. Still zero (or unhealable) => the product is HELD as draft with 'weight>0',
never activated. Idempotent: a fully weighted product makes zero extra API calls.
Each site keeps its own style (.js/.mjs, its own gql, its own result shape — fallingstar
returns the errs array its caller reads, the rest return { heldZeroWeight, errs }). No
inventory, price or publication logic touched; the TK-11357 /bin/zsh guard is untouched.
verification/tk11471/golive-wiring.test.mjs asserts all ten sites are wired AND carries five
negative tests (drop the import / drop measurement.weight / drop productType / delete the gate /
move it after the mutation) that each prove the detector goes red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJHxAzaEMMxado57mFjiCk
---
scripts/artmura-onboard/go-live-artmura.js | 12 ++-
scripts/fallingstar-onboard/go-live-fallingstar.js | 12 ++-
scripts/knoll-onboard/go-live.mjs | 12 ++-
scripts/maharam-onboard/go-live.mjs | 12 ++-
scripts/muralsource-onboard/go-live.mjs | 12 ++-
scripts/osborne-onboard/go-live.mjs | 12 ++-
scripts/stout-onboard/go-live.mjs | 12 ++-
scripts/stroheim-onboard/go-live.mjs | 12 ++-
scripts/tres-tintas-onboard/go-live-tres-tintas.js | 12 ++-
verification/tk11471/golive-wiring.test.mjs | 103 +++++++++++++++++++++
10 files changed, 202 insertions(+), 9 deletions(-)
diff --git a/scripts/artmura-onboard/go-live-artmura.js b/scripts/artmura-onboard/go-live-artmura.js
index ad007fe..0d9ed11 100644
--- a/scripts/artmura-onboard/go-live-artmura.js
+++ b/scripts/artmura-onboard/go-live-artmura.js
@@ -62,8 +62,9 @@ async function gql(query, variables = {}) {
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+const { healAndVerifyWeights } = require('../lib/weight-guard.mjs'); // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -127,6 +128,15 @@ async function goLive(pid) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gidP);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
return { status: r4.productUpdate?.product?.status, errs };
diff --git a/scripts/fallingstar-onboard/go-live-fallingstar.js b/scripts/fallingstar-onboard/go-live-fallingstar.js
index aae60e3..022e344 100644
--- a/scripts/fallingstar-onboard/go-live-fallingstar.js
+++ b/scripts/fallingstar-onboard/go-live-fallingstar.js
@@ -56,8 +56,9 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+const { healAndVerifyWeights } = require('../lib/weight-guard.mjs'); // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -118,6 +119,15 @@ async function goLive(pid) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gidP);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return [...errs, 'heldZeroWeight: weight>0']; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
return errs;
diff --git a/scripts/knoll-onboard/go-live.mjs b/scripts/knoll-onboard/go-live.mjs
index aac6e05..7b76147 100644
--- a/scripts/knoll-onboard/go-live.mjs
+++ b/scripts/knoll-onboard/go-live.mjs
@@ -37,8 +37,9 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType handle vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -116,6 +117,15 @@ async function goLive(pid, sku) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gidP);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
const status = r4.productUpdate?.product?.status;
diff --git a/scripts/maharam-onboard/go-live.mjs b/scripts/maharam-onboard/go-live.mjs
index dd989c7..28d8314 100644
--- a/scripts/maharam-onboard/go-live.mjs
+++ b/scripts/maharam-onboard/go-live.mjs
@@ -40,8 +40,9 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType handle vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -121,6 +122,15 @@ async function goLive(pid, sku, mfrSku) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gidP);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
const status = r4.productUpdate?.product?.status;
diff --git a/scripts/muralsource-onboard/go-live.mjs b/scripts/muralsource-onboard/go-live.mjs
index 3753c22..37346a0 100644
--- a/scripts/muralsource-onboard/go-live.mjs
+++ b/scripts/muralsource-onboard/go-live.mjs
@@ -45,8 +45,9 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType handle vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -134,6 +135,15 @@ async function goLive(pid, sku, mfrSku) {
if (!((_mf?.product?.c?.value || _mf?.product?.d?.value || '').trim())) {
console.error(' ⛔ HELD (no manufacturer_sku metafield — TK-10303 go-live gate): ' + gidP);
return { heldNoMfr: true }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
const status = r4.productUpdate?.product?.status;
diff --git a/scripts/osborne-onboard/go-live.mjs b/scripts/osborne-onboard/go-live.mjs
index 6608384..87dd258 100644
--- a/scripts/osborne-onboard/go-live.mjs
+++ b/scripts/osborne-onboard/go-live.mjs
@@ -51,8 +51,9 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -114,6 +115,15 @@ async function goLive(pid) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gidP);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
return { status: r4.productUpdate?.product?.status, errs };
diff --git a/scripts/stout-onboard/go-live.mjs b/scripts/stout-onboard/go-live.mjs
index f429d8d..69d7219 100644
--- a/scripts/stout-onboard/go-live.mjs
+++ b/scripts/stout-onboard/go-live.mjs
@@ -48,8 +48,9 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType handle vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -137,6 +138,15 @@ async function goLive(pid, sku, mfrSku) {
if (!((_mf?.product?.c?.value || _mf?.product?.d?.value || '').trim())) {
console.error(' ⛔ HELD (no manufacturer_sku metafield — TK-10303 go-live gate): ' + gidP);
return { heldNoMfr: true }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
const status = r4.productUpdate?.product?.status;
diff --git a/scripts/stroheim-onboard/go-live.mjs b/scripts/stroheim-onboard/go-live.mjs
index 9985a26..f6d2c6e 100644
--- a/scripts/stroheim-onboard/go-live.mjs
+++ b/scripts/stroheim-onboard/go-live.mjs
@@ -50,8 +50,9 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status productType handle vendor tags variants(first:10){edges{node{sku title price inventoryItem{id measurement{weight{value unit}}}}}}}}`;
import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+import { healAndVerifyWeights } from '../lib/weight-guard.mjs'; // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -138,6 +139,15 @@ async function goLive(pid, sku, mfrSku) {
if (!((_mf?.product?.c?.value || _mf?.product?.d?.value || '').trim())) {
console.error(' ⛔ HELD (no manufacturer_sku metafield — TK-10303 go-live gate): ' + gidP);
return { heldNoMfr: true }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gidP, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gidP + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const r4 = await gql(M_ACTIVE, { id: gidP });
(r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
const status = r4.productUpdate?.product?.status;
diff --git a/scripts/tres-tintas-onboard/go-live-tres-tintas.js b/scripts/tres-tintas-onboard/go-live-tres-tintas.js
index 2be4bb8..5e93b85 100644
--- a/scripts/tres-tintas-onboard/go-live-tres-tintas.js
+++ b/scripts/tres-tintas-onboard/go-live-tres-tintas.js
@@ -58,8 +58,9 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_V = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id title price inventoryItem{id}}}} } }`;
+const Q_V = `query($id:ID!){ product(id:$id){ status productType vendor tags variants(first:10){edges{node{sku id title price inventoryItem{id measurement{weight{value unit}}}}}} } }`;
const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+const { healAndVerifyWeights } = require('../lib/weight-guard.mjs'); // GUARD TK-11471 (weight go-live: self-heal then verify)
// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
@@ -120,6 +121,15 @@ async function goLive(pid) {
if (!((_dh?.product?.descriptionHtml || '').replace(/<[^>]*>/g, '').trim())) {
console.error(' ⛔ HELD (empty description — TK-11237 go-live gate): ' + gid);
return { heldNoDesc: true, errs: ['heldNoDesc: empty description'] }; } }
+ // TK-11471 (TK-11414 follow-on) WEIGHT go-live gate: never flip a product ACTIVE at zero weight —
+ // zero collapses the order into the lowest weight tier / free-shipping band and mis-costs DW
+ // freight. SELF-HEAL first (assign the already-approved default rather than strand the product,
+ // per sanderson-onboard create_sdg.mjs 8d09eed), then RE-VERIFY; still zero ⇒ HOLD as draft.
+ { const _w = await healAndVerifyWeights(gql, gid, d.product);
+ _w.errs.forEach(e => errs.push(e));
+ if (!_w.ok) {
+ console.error(' ⛔ HELD (weight>0 — TK-11414 weight go-live gate): ' + gid + (_w.stillZero.length ? ' [' + _w.stillZero.join(',') + ']' : ''));
+ return { heldZeroWeight: true, errs: [...errs, 'heldZeroWeight: weight>0'] }; } }
const ra = await gql(M_ACTIVE, { id: gid });
(ra.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
return { errs };
diff --git a/verification/tk11471/golive-wiring.test.mjs b/verification/tk11471/golive-wiring.test.mjs
new file mode 100644
index 0000000..2b3ee09
--- /dev/null
+++ b/verification/tk11471/golive-wiring.test.mjs
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/**
+ * golive-wiring.test.mjs — TK-11471. Offline static assertion that EVERY go-live site is wired to
+ * the weight gate. node verification/tk11471/golive-wiring.test.mjs
+ *
+ * This is the check that catches the specific false-green named in the brief: a gate that runs
+ * against a product query which does NOT select inventoryItem{measurement{weight}} measures
+ * nothing and silently passes. So the wiring test asserts FOUR things per site, not one:
+ * 1. it imports healAndVerifyWeights from the shared module (no re-implementation)
+ * 2. the product query selects measurement{weight{...}} — the guard can SEE the weight
+ * 3. the product query selects productType — the heal picks the right default,
+ * not a blanket FALLBACK_LB
+ * 4. the gate call sits BEFORE the status:ACTIVE mutation
+ *
+ * Reads source only. No network, no DB, no Shopify.
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts');
+
+// Every script in scripts/*-onboard/ that flips a product to status:ACTIVE.
+export const GOLIVE_SITES = [
+ 'artmura-onboard/go-live-artmura.js',
+ 'fallingstar-onboard/go-live-fallingstar.js',
+ 'knoll-onboard/go-live.mjs',
+ 'maharam-onboard/go-live.mjs',
+ 'muralsource-onboard/go-live.mjs',
+ 'osborne-onboard/go-live.mjs',
+ 'sanderson-onboard/go-live.mjs', // pre-existing gate (validate() 'weight>0'), the reference
+ 'stout-onboard/go-live.mjs',
+ 'stroheim-onboard/go-live.mjs',
+ 'tres-tintas-onboard/go-live-tres-tintas.js',
+];
+
+/** The detector, applied to SOURCE TEXT so it can also be run against an injected fault. */
+export function auditSource(src) {
+ const importsGuard = /from\s+['"][^'"]*lib\/weight-guard\.mjs['"]/.test(src)
+ || /require\(['"][^'"]*lib\/weight-guard\.mjs['"]\)/.test(src);
+ const selectsWeight = /measurement\s*\{\s*weight\s*\{/.test(src);
+ const selectsProductType = /product\(id:\$id\)\{[^`]*productType/.test(src);
+ const healsInPlace = /healAndVerifyWeights\s*\(/.test(src); // the heal needs productType to pick a default
+ const usesGate = healsInPlace || /hasZeroWeight\s*\(/.test(src);
+ const iGate = Math.max(src.indexOf('healAndVerifyWeights('), src.indexOf('hasZeroWeight('));
+ // Compare against the mutation CALL SITE, not the `status:ACTIVE` text inside the M_ACTIVE
+ // constant near the top of the file — that constant always precedes the gate and would make
+ // this assertion meaningless (it did, on the first cut).
+ const iActive = src.indexOf('gql(M_ACTIVE');
+ const gateBeforeActivate = usesGate && iGate >= 0 && iActive >= 0 && iGate < iActive;
+ const missing = [];
+ if (!importsGuard) missing.push('import weight-guard');
+ if (!selectsWeight) missing.push('query selects measurement.weight');
+ if (healsInPlace && !selectsProductType) missing.push('query selects productType');
+ if (!usesGate) missing.push('calls the gate');
+ if (!gateBeforeActivate) missing.push('gate precedes status:ACTIVE');
+ return { ok: missing.length === 0, missing };
+}
+
+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 : ''}`); } };
+
+console.log('── every go-live site is wired to the weight gate');
+for (const rel of GOLIVE_SITES) {
+ const p = path.join(ROOT, rel);
+ if (!fs.existsSync(p)) { fail++; console.error(` FAIL ${rel} — file missing`); continue; }
+ const r = auditSource(fs.readFileSync(p, 'utf8'));
+ ok(rel, r.ok, r.missing.join(', '));
+}
+
+console.log('\n── NEGATIVE: the detector goes red on each injected fault');
+{
+ const good = fs.readFileSync(path.join(ROOT, 'osborne-onboard/go-live.mjs'), 'utf8');
+ ok('control: the real file passes', auditSource(good).ok);
+
+ // fault 1 — the import is removed (someone re-implements the check inline)
+ const f1 = good.replace(/^import \{ healAndVerifyWeights \}.*$/m, '');
+ ok('removing the import => red', !auditSource(f1).ok && auditSource(f1).missing.includes('import weight-guard'));
+
+ // fault 2 — THE FALSE-GREEN CLASS: the query stops selecting the weight, so the guard
+ // measures nothing. This is exactly what the brief warned about.
+ const f2 = good.replace(' measurement{weight{value unit}}', '');
+ ok('query stops selecting measurement.weight => red',
+ !auditSource(f2).ok && auditSource(f2).missing.includes('query selects measurement.weight'));
+
+ // fault 3 — productType dropped, so every heal would silently use FALLBACK_LB
+ const f3 = good.replace('status productType vendor', 'status vendor');
+ ok('query stops selecting productType => red',
+ !auditSource(f3).ok && auditSource(f3).missing.includes('query selects productType'));
+
+ // fault 4 — the gate call is deleted entirely
+ const f4 = good.replace(/\{ const _w = await healAndVerifyWeights[\s\S]*?heldZeroWeight: weight>0'\] \}; \} \}/, '');
+ ok('deleting the gate call => red', !auditSource(f4).ok, JSON.stringify(auditSource(f4)));
+
+ // fault 5 — the gate is moved AFTER the activate mutation (present, but useless)
+ const moved = good.replace(/(\{ const _w = await healAndVerifyWeights[\s\S]*?heldZeroWeight: weight>0'\] \}; \} \})\n( const r4 = await gql\(M_ACTIVE[^\n]*\n)/, '$2$1\n');
+ ok('moving the gate after status:ACTIVE => red',
+ !auditSource(moved).ok && auditSource(moved).missing.includes('gate precedes status:ACTIVE'),
+ JSON.stringify(auditSource(moved)));
+}
+
+console.log(`\n${fail === 0 ? 'PASS' : 'FAIL'} — ${pass} passed, ${fail} failed`);
+process.exit(fail === 0 ? 0 : 1);
← 8dc084a TK-11461: Sanderson gallery contamination root cause — input
·
back to Designerwallcoverings
·
TK-11471: every create payload carries a guard-sourced weigh 231a90e →