[object Object]

← back to Ticket System

auto-data-snapshot: 2026-09-17T17:25:36 (9 data files) — evidence/TK-11871/TK-11871-verdict.json evidence/TK-11871/restore-map-20260917-1700/RESTORE.md evidence/TK-11871/restore-map-20260917-1700/test_weight_guard.py.ORIG evidence/TK-11871/restore-map-20260917-1700/weight-guard-DesignerWallcoverings.mjs.ORIG evidence/TK-11871/restore-map-20260917-1700/weight-guard.mjs.ORIG

018f10f5480525bfbbfc55c49082d4d54a3e49fa · 2026-09-17 17:25:37 -0700 · auto-commit-fleet

Files touched

Diff

commit 018f10f5480525bfbbfc55c49082d4d54a3e49fa
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date:   Thu Sep 17 17:25:37 2026 -0700

    auto-data-snapshot: 2026-09-17T17:25:36 (9 data files) — evidence/TK-11871/TK-11871-verdict.json evidence/TK-11871/restore-map-20260917-1700/RESTORE.md evidence/TK-11871/restore-map-20260917-1700/test_weight_guard.py.ORIG evidence/TK-11871/restore-map-20260917-1700/weight-guard-DesignerWallcoverings.mjs.ORIG evidence/TK-11871/restore-map-20260917-1700/weight-guard.mjs.ORIG
---
 evidence/TK-11871/TK-11871-verdict.json            |  10 +
 .../TK-11871/restore-map-20260917-1700/RESTORE.md  |  36 +++
 .../test_weight_guard.py.ORIG                      | 253 +++++++++++++++++
 .../weight-guard-DesignerWallcoverings.mjs.ORIG    | 168 ++++++++++++
 .../weight-guard.mjs.ORIG                          | 168 ++++++++++++
 .../weight-guard.test.mjs.ORIG                     | 206 ++++++++++++++
 .../weight-guard.test.ts.ORIG                      |  96 +++++++
 .../restore-map-20260917-1700/weight-guard.ts.ORIG |  74 +++++
 .../restore-map-20260917-1700/weight_guard.py.ORIG | 299 +++++++++++++++++++++
 9 files changed, 1310 insertions(+)

diff --git a/evidence/TK-11871/TK-11871-verdict.json b/evidence/TK-11871/TK-11871-verdict.json
new file mode 100644
index 00000000..d8cb4230
--- /dev/null
+++ b/evidence/TK-11871/TK-11871-verdict.json
@@ -0,0 +1,10 @@
+{
+  "ticket": "TK-11871",
+  "action": "TYPE_DEFAULT_LB Wallcovering family 3.0 -> 2.0 across guard sync-group (mjs x2, py, ts) + tests aligned (mjs/py/ts) + 2 comment corrections (batch-import-jobs.ts, dwpw-grs-migrate.py)",
+  "class": "go-forward code default; NO Shopify/dw_unified writes; NOT customer-facing (no historical re-stamp)",
+  "rationale": "TK-11803 DTD verdict A + Cody red-team ratified 2.0 = live catalog norm (91.9% of 2,299-sample); TK-11871 is the sanctioned split-out; ticket text: 'align the guard to catalog 2.0' is an explicitly allowed resolution",
+  "blast_radius": "new Wallcovering-type creates/heals only (Morris/Zoffany active importers use vendor scripts at 4lb; cadence 15-18 rows/2x daily)",
+  "tests": {"py": "14/14 OK", "mjs": "52/52 PASS", "jest_weight_guard_ts": "10/10", "jest_batch_import_gate": "36/36", "tsc": "clean", "cadence_syntax": "OK"},
+  "restore_map": "evidence/TK-11871/restore-map-20260917-1700/",
+  "date": "2026-09-17T17:35:00-07:00"
+}
diff --git a/evidence/TK-11871/restore-map-20260917-1700/RESTORE.md b/evidence/TK-11871/restore-map-20260917-1700/RESTORE.md
new file mode 100644
index 00000000..d906e1e1
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/RESTORE.md
@@ -0,0 +1,36 @@
+# TK-11871 Restore Map — TYPE_DEFAULT_LB Wallcovering 3.0 → 2.0 (2026-09-17 ~17:20 PDT)
+
+## What changed
+Wallcovering-family per-type default weight in the weight-guard sync-group:
+3.0 → 2.0 lb (catalog norm; TK-11803 DTD verdict A, 91.9% of ACTIVE Wallcovering
+sellable variants). Revert = set the family back to 3.0.
+
+## Files changed (8)
+| File | Change | Restore |
+|---|---|---|
+| ~/Projects/designerwallcoverings/scripts/lib/weight-guard.mjs | Wallcovering family 3.0→2.0 + TK-11871 comment above export | weight-guard.mjs.ORIG |
+| ~/Projects/Designer-Wallcoverings/shopify/scripts/lib/weight-guard.mjs | same (bit-identical twin) | weight-guard-DesignerWallcoverings.mjs.ORIG |
+| ~/Projects/designerwallcoverings/scripts/lib/weight_guard.py | same (python twin) | weight_guard.py.ORIG |
+| ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts | same (ts mirror) | weight-guard.ts.ORIG |
+| ~/Projects/designerwallcoverings/scripts/lib/weight-guard.test.mjs | 3 assertions 3→2 (+1 new literal-2.0 case) | weight-guard.test.mjs.ORIG |
+| ~/Projects/designerwallcoverings/scripts/lib/test_weight_guard.py | 2 assertions + 1 derived-weight 3→2 | test_weight_guard.py.ORIG |
+| ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.ts | 4 assertions 3→2 | weight-guard.test.ts.ORIG |
+| ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/lib/batch-import-jobs.ts | comment-only ("2.0 lb default (TK-11871)") | not versioned in this dir (uncommitted TK-11786 work present); change is a comment — revert text "2.0 lb Wallcovering default (TK-11871: catalog norm; was 3.0)" → "3.0 lb Wallcovering default" |
+| ~/Projects/designerwallcoverings/scripts/dwpw-grs-migrate.py | comment-only ("2.0 lb — TK-11871 catalog norm, was 3.0") | restore via git checkout (file committed at 4d31a1a/67d7eb3; docstring edit only) |
+
+## Restore commands
+cp <file>.ORIG back over the live file (paths above), or:
+  cd ~/Projects/designerwallcoverings && git checkout -- scripts/lib/weight-guard.mjs scripts/lib/weight_guard.py scripts/lib/weight-guard.test.mjs scripts/lib/test_weight_guard.py scripts/dwpw-grs-migrate.py
+  cd ~/Projects/Designer-Wallcoverings && git checkout -- shopify/scripts/lib/weight-guard.mjs DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.ts
+(batch-import-jobs.ts has PRE-EXISTING uncommitted TK-11786 work — do NOT git-checkout it; hand-revert only the one comment line.)
+
+## Scope invariants (verified)
+- Samples untouched: 0.25 lb everywhere (SAMPLE_WEIGHT_LB unchanged).
+- FALLBACK_LB unchanged at 2.0 (now == Wallcovering default, so unknown-type and Wallcovering agree).
+- Non-Wallcovering types unchanged (Fabric 1, Mural 4, Trim 0.5, Acoustic Panel 6, Pillow 1.5, Upholstered Walls 6, Tin 2, Hardware 1, Furniture 15, Memo 0.25).
+- NO Shopify writes. NO dw_unified writes. Local code + tests only.
+
+## Verification at apply time
+- py suite 14/14 OK · mjs suite 52/52 PASS · jest weight-guard 10/10 · jest batch-import-gate 36/36 (pre-existing TK-11786 state) · tsc --noEmit clean · cadence-import.js node --check OK
+- mjs twins shasum-identical after edit (350399246aa8c5150c0cc837daa244cfae38b306)
+- Derived consumers verified: dwpw-grs-migrate._WEIGHT_SELLABLE_LB=2.0, cadence resolveWeightLb→2, sanderson build-payloads→2, batch-import sellableWeightLb→2, tk11471 backfill target→2
diff --git a/evidence/TK-11871/restore-map-20260917-1700/test_weight_guard.py.ORIG b/evidence/TK-11871/restore-map-20260917-1700/test_weight_guard.py.ORIG
new file mode 100644
index 00000000..941bfed5
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/test_weight_guard.py.ORIG
@@ -0,0 +1,253 @@
+#!/usr/bin/env python3
+"""test_weight_guard.py — TK-11471. OFFLINE: zero network, zero DB, zero cost.
+
+    python3 scripts/lib/test_weight_guard.py
+
+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.
+
+Covers the Python guard (lib/weight_guard.py) AND the real publish path in
+dwpw-grs-migrate.py — the job that actually created 80 zero-weight ACTIVE variants
+on 2026-09-11 (all 40 products appear in dwpw-grs-migrate.lastrun.json with
+result=PUBLISHED_NO_TWIN, 37 ensure=create + 3 ensure=update).
+"""
+import importlib.util
+import os
+import tempfile
+import sys
+import unittest
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SCRIPTS = os.path.dirname(HERE)
+sys.path.insert(0, HERE)
+
+import weight_guard as wg  # noqa: E402
+
+MJS = os.path.join(HERE, "weight-guard.mjs")
+
+
+def sellable(**kw):
+    v = {"id": "gid://shopify/ProductVariant/1", "sku": "GRS-27580", "title": "Per Yard",
+         "price": "63.00"}
+    v.update(kw)
+    return v
+
+
+def sample(**kw):
+    v = {"id": "gid://shopify/ProductVariant/2", "sku": "GRS-27580-Sample", "title": "Sample",
+         "price": "4.25"}
+    v.update(kw)
+    return v
+
+
+def node(vid, sku, title, lb, iid=None, drop_field=False):
+    inv = {"id": iid or ("gid://shopify/InventoryItem/%s" % vid)}
+    if not drop_field:
+        inv["measurement"] = None if lb is None else {"weight": {"value": lb, "unit": "POUNDS"}}
+    return {"id": "gid://shopify/ProductVariant/%s" % vid, "sku": sku, "title": title,
+            "price": "4.25" if "Sample" in title else "63.00", "inventoryItem": inv}
+
+
+WALLCOVERING = {"product_type": "Wallcovering"}
+
+
+class TestDetector(unittest.TestCase):
+    def test_injected_zero_weight_sellable_is_flagged(self):
+        p = {"productType": "Wallcovering",
+             "variants": [node(1, "GRS-27580", "Per Yard", 0), node(2, "GRS-27580-Sample", "Sample", 0.25)]}
+        self.assertEqual([v["sku"] for v in wg.all_zero_weight_variants(p)], ["GRS-27580"])
+
+    def test_injected_zero_weight_SAMPLE_is_flagged(self):
+        """dw-active-weight-canary FAILs on a zero-weight SAMPLE too (its live run split
+        the offenders 43 sample / 40 sellable). The .mjs zeroWeightBlockers() filters
+        samples out; that bug must not exist here."""
+        p = {"productType": "Wallcovering",
+             "variants": [node(1, "GRS-27580", "Per Yard", 3.0), node(2, "GRS-27580-Sample", "Sample", 0)]}
+        self.assertEqual([v["sku"] for v in wg.all_zero_weight_variants(p)], ["GRS-27580-Sample"])
+
+    def test_missing_weight_field_is_flagged_fails_safe(self):
+        p = {"productType": "Wallcovering", "variants": [sellable(), sample()]}
+        self.assertEqual(len(wg.all_zero_weight_variants(p)), 2)
+        self.assertEqual(len(wg.unmeasured_variants(p)), 2)
+
+    def test_none_and_junk_weight_are_flagged(self):
+        self.assertTrue(wg.has_zero_weight(sellable(weight=None)))
+        self.assertTrue(wg.has_zero_weight(sellable(weight="junk", weight_unit="lb")))
+        self.assertTrue(wg.has_zero_weight(node(1, "GRS-1", "Per Yard", None)))
+
+    def test_units_convert_and_positive_is_not_flagged(self):
+        self.assertAlmostEqual(wg.current_weight_lb(node(1, "x", "Per Yard", 1361, )), 1361)   # POUNDS as given
+        self.assertAlmostEqual(wg.current_weight_lb(sellable(weight=1361, weight_unit="g")), 3.0, places=2)
+        self.assertAlmostEqual(wg.current_weight_lb(sellable(weight=1.36, weight_unit="kg")), 2.998, places=2)
+        self.assertAlmostEqual(wg.current_weight_lb(sellable(weight=48, weight_unit="oz")), 3.0, places=3)
+        self.assertFalse(wg.has_zero_weight(sellable(weight=1361, weight_unit="g")))
+        v = {"id": "v", "sku": "x", "title": "Per Yard",
+             "inventoryItem": {"id": "i", "measurement": {"weight": {"value": 1361, "unit": "GRAMS"}}}}
+        self.assertAlmostEqual(wg.current_weight_lb(v), 3.0, places=2)
+        self.assertFalse(wg.has_zero_weight(v))
+
+    def test_all_weighted_returns_empty(self):
+        p = {"productType": "Wallcovering",
+             "variants": [node(1, "GRS-27580", "Per Yard", 3.0), node(2, "GRS-27580-Sample", "Sample", 0.25)]}
+        self.assertEqual(wg.all_zero_weight_variants(p), [])
+
+    def test_resolve_preserves_positive_and_fills_default(self):
+        self.assertAlmostEqual(wg.resolve_weight_lb(sellable(weight=7.5, weight_unit="lb"), WALLCOVERING), 7.5)
+        self.assertEqual(wg.resolve_weight_lb(sellable(), WALLCOVERING), 3.0)
+        self.assertEqual(wg.resolve_weight_lb(sample(), WALLCOVERING), 0.25)
+        self.assertEqual(wg.resolve_weight_lb(sellable(), {"product_type": "Mural"}), 4.0)
+        self.assertEqual(wg.resolve_weight_lb(sellable(), {"product_type": "Nonesuch"}), 2.0)
+
+    @unittest.skipUnless(os.path.exists(MJS), "weight-guard.mjs missing")
+    def test_constants_match_the_mjs_twin(self):
+        with open(MJS) as fh:
+            src = fh.read()
+        self.assertIn("SAMPLE_WEIGHT_LB = 0.25", src)
+        self.assertIn("FALLBACK_LB = 2.0", src)
+        table = src.split("TYPE_DEFAULT_LB = {", 1)[1].split("};", 1)[0]
+        pairs = {}
+        for chunk in table.replace("\n", " ").split(","):
+            if ":" not in chunk:
+                continue
+            k, v = chunk.split(":", 1)
+            try:
+                pairs[k.strip().strip("'\"")] = float(v.strip().rstrip(","))
+            except ValueError:
+                pass
+        self.assertEqual(pairs, wg.TYPE_DEFAULT_LB, "TYPE_DEFAULT_LB drifted from weight-guard.mjs")
+
+
+def load_migrate():
+    os.environ.setdefault("SHOPIFY_FULL_ACCESS_TOKEN", "test-token-not-real")
+    spec = importlib.util.spec_from_file_location("dwpw_grs_migrate",
+                                                  os.path.join(SCRIPTS, "dwpw-grs-migrate.py"))
+    m = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(m)
+    return m
+
+
+class TestMigrateWeightsAreStamped(unittest.TestCase):
+    def test_create_payload_weights_come_from_the_guard(self):
+        m = load_migrate()
+        self.assertEqual(m._WEIGHT_SELLABLE_LB, 3.0)     # Wallcovering default
+        self.assertEqual(m._WEIGHT_SAMPLE_LB, 0.25)      # sample default
+        self.assertGreater(m._WEIGHT_SELLABLE_LB, 0)
+        self.assertGreater(m._WEIGHT_SAMPLE_LB, 0)
+
+    def test_every_variant_mutation_carries_a_weight(self):
+        """INJECTED-FAULT SHAPE CHECK on the real shipped source: no
+        productVariantsBulk{Create,Update} may send an inventoryItem without a
+        measurement.weight — that is exactly how 80 variants shipped at zero."""
+        with open(os.path.join(SCRIPTS, "dwpw-grs-migrate.py")) as fh:
+            src = fh.read()
+        chunks = [c for c in src.split('"inventoryItem": {')[1:]]
+        self.assertGreaterEqual(len(chunks), 4, "expected >=4 variant inventoryItem payloads")
+        for c in chunks:
+            head = c[:260]
+            self.assertIn("measurement", head,
+                          "an inventoryItem variant payload ships with no weight:\n" + head)
+
+
+class TestMigratePublishGate(unittest.TestCase):
+    """Mocked harness over the REAL process() apply path. No network, no Shopify."""
+
+    ROW = {"grs": "GRS-27580", "mfr": "T-1", "title": "Oliva Khaki Grasscloth Wallcovering",
+           "image": "https://example.invalid/x.jpg", "dw_price": "63.00", "cost_yd": "21.00"}
+
+    def _harness(self, heal_ok):
+        m = load_migrate()
+        state = {"weights": {"gid://shopify/InventoryItem/1": 0.0,
+                             "gid://shopify/InventoryItem/2": 0.0},
+                 "published": False, "archived": False, "mutations": []}
+
+        def fake_gql(query, variables=None):
+            variables = variables or {}
+            if "inventoryItemUpdate" in query:
+                state["mutations"].append((variables["id"], variables["w"]))
+                if not heal_ok:
+                    return {"data": {"inventoryItemUpdate":
+                                     {"userErrors": [{"message": "injected heal failure"}]}}}
+                state["weights"][variables["id"]] = variables["w"]
+                return {"data": {"inventoryItemUpdate": {"userErrors": []}}}
+            # WEIGHT_REQUERY — the response DOES carry the weight field
+            edges = []
+            for iid, lb in state["weights"].items():
+                is_sample = iid.endswith("/2")
+                edges.append({"node": node(iid.rsplit("/", 1)[-1],
+                                           "GRS-27580-Sample" if is_sample else "GRS-27580",
+                                           "Sample" if is_sample else "Per Yard",
+                                           None if lb <= 0 else lb, iid=iid)})
+            return {"data": {"product": {"productType": "Wallcovering",
+                                         "variants": {"edges": edges}}}}
+
+        def fake_publish(pid, grs, prev_status):
+            state["published"] = True
+
+        m.gql = fake_gql
+        m.find_grs = lambda grs: None
+        m.http_status = lambda url, method="GET": 200
+        m.find_active_dwpw_twin = lambda mfr: []
+        m.create_grs = lambda row, image_ok, res: ("gid://shopify/Product/9", "oliva-khaki")
+        m.verify_read = lambda pid, grs, **kw: {"variants": [sellable(), sample()]}
+        m.verify_image = lambda pid, url, **kw: (True, "READY", ["READY"])
+        m.publish_active = fake_publish
+        m.archive_dwpw = lambda twin, grs: state.__setitem__("archived", True)
+        # HERMETICITY (fixed 2026-09-12): require_no_prior_creation() reads the SHARED, LIVE,
+        # append-only executed-reversible ledger. The fixture SKU GRS-27580 is a REAL sku, so once
+        # the live migration recorded creation receipts for it the guard began raising HISTORY_HOLD
+        # and these 4 tests errored — they passed on 2026-09-11 and failed on 2026-09-12 with ZERO
+        # code change between, purely because production state moved underneath them.
+        #
+        # That is the same defect class this whole test file exists to prevent, one level up: a
+        # check whose verdict is decided by something it does not actually measure. A test that can
+        # silently flip on unrelated live activity is worthless as a signal in BOTH directions —
+        # it can go red on healthy code, and it can be dismissed as "just that flaky one" when it
+        # goes red for a real reason.
+        #
+        # Point the guard at an EMPTY temp ledger. The guard itself is NOT stubbed out — it still
+        # runs for real against a controlled input, so a genuine duplicate-identity regression is
+        # still caught; only the shared live file is swapped for a deterministic one.
+        # (Verified while fixing: GRS-27580 has exactly ONE live product, so the 3 ledger receipts
+        # were historical/archived, not a real customer-facing duplicate.)
+        tmp = tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False)
+        tmp.close()
+        self.addCleanup(os.unlink, tmp.name)
+        m.LEDGER = tmp.name
+        return m, state
+
+    def test_zero_weight_product_is_HEALED_then_PUBLISHED(self):
+        m, state = self._harness(heal_ok=True)
+        res = m.process(dict(self.ROW), apply=True)
+        self.assertEqual(res["result"], "PUBLISHED_NO_TWIN", res)
+        self.assertTrue(state["published"])
+        self.assertEqual(sorted(w for _, w in state["mutations"]), [0.25, 3.0])
+
+    def test_product_whose_heal_FAILS_is_NOT_PUBLISHED(self):
+        m, state = self._harness(heal_ok=False)
+        res = m.process(dict(self.ROW), apply=True)
+        self.assertEqual(res["result"], "LEFT_DRAFT_NEEDS_WEIGHT", res)
+        self.assertFalse(state["published"], "a zero-weight product must NEVER be published ACTIVE")
+        self.assertFalse(state["archived"], "the DWPW twin must not be archived on a held GRS")
+        self.assertTrue(any("weight>0" in r for r in res["weight_block"]), res.get("weight_block"))
+
+    def test_gate_fails_closed_when_the_requery_carries_no_weight_field(self):
+        """MEASURE WHAT YOU CLAIM — a response that never contained weight must not
+        be read as a clean pass."""
+        m, state = self._harness(heal_ok=True)
+        m.gql = lambda q, v=None: {"data": {"product": {"productType": "Wallcovering", "variants": {
+            "edges": [{"node": node(1, "GRS-27580", "Per Yard", 0, drop_field=True)}]}}}}
+        res = m.process(dict(self.ROW), apply=True)
+        self.assertEqual(res["result"], "LEFT_DRAFT_NEEDS_WEIGHT", res)
+        self.assertFalse(state["published"])
+
+    def test_gate_is_a_noop_when_weights_are_already_positive(self):
+        m, state = self._harness(heal_ok=True)
+        state["weights"] = {"gid://shopify/InventoryItem/1": 3.0,
+                            "gid://shopify/InventoryItem/2": 0.25}
+        res = m.process(dict(self.ROW), apply=True)
+        self.assertEqual(res["result"], "PUBLISHED_NO_TWIN", res)
+        self.assertEqual(state["mutations"], [], "idempotent: no heal writes when already weighted")
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)
diff --git a/evidence/TK-11871/restore-map-20260917-1700/weight-guard-DesignerWallcoverings.mjs.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight-guard-DesignerWallcoverings.mjs.ORIG
new file mode 100644
index 00000000..8d8313d0
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight-guard-DesignerWallcoverings.mjs.ORIG
@@ -0,0 +1,168 @@
+// weight-guard.mjs — TK-11414 (2026-09-10) prevention primitive.
+// Steve's rule: NO product may go ACTIVE with a missing/zero product WEIGHT
+// (zero-weight collapses orders into the lowest weight tier / free band and
+// 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 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.
+
+export const SAMPLE_WEIGHT_LB = 0.25;
+export const FALLBACK_LB = 2.0;
+
+// product_type -> sellable default weight (POUNDS)
+export const TYPE_DEFAULT_LB = {
+  'Wallcovering': 3.0, 'Wallcoverings': 3.0, 'Wallpaper': 3.0,
+  'Metallic Wallcovering': 3.0, 'Commercial Wallcovering': 3.0,
+  'Mural': 4.0,
+  'Fabric': 1.0, 'Commercial Fabric': 1.0, 'Commercial Drapery': 1.0,
+  'Trim': 0.5, 'Acoustic Panel': 6.0, 'Pillow': 1.5,
+  'Upholstered Walls/Panels': 6.0, 'Tin Ceiling Tile': 2.0,
+  'Hardware': 1.0, 'Furniture': 15.0, 'Memo Sample': 0.25,
+};
+
+const norm = t => String(t ?? '').trim().toLowerCase();
+
+/** Sample variant? (importers create `Sample` @ $4.25 + the real unit). */
+export function isSampleVariant(variant = {}) {
+  const label = norm(variant.title ?? variant.option1 ?? '');
+  const sku = norm(variant.sku);
+  if (label.includes('sample') || label.includes('memo')) return true;
+  if (sku.endsWith('-sample') || sku.includes('sample')) return true;
+  const p = Number(variant.price);
+  return Number.isFinite(p) && Math.abs(p - 4.25) < 0.01;
+}
+
+/** Current weight in lb, or 0 if missing/unparseable. Accepts variant.weight (REST),
+ *  variant.grams, or inventoryItem.measurement.weight.value (GraphQL). */
+export function currentWeightLb(variant = {}) {
+  const gql = variant?.inventoryItem?.measurement?.weight;
+  if (gql && gql.value != null) {
+    const v = Number(gql.value);
+    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) {
+    const v = Number(variant.weight);
+    const u = norm(variant.weight_unit || 'lb');
+    if (u.startsWith('kg')) return v * 2.20462;
+    if (u === 'g' || u.startsWith('gram')) return v / 453.59237;
+    if (u === 'oz') return v / 16;
+    return v; // lb
+  }
+  return 0;
+}
+
+export function hasZeroWeight(variant = {}) {
+  const w = currentWeightLb(variant);
+  return !Number.isFinite(w) || w <= 0;
+}
+
+/** The default weight (lb) to assign a variant that has none. */
+export function defaultWeightLb(variant = {}, product = {}) {
+  if (isSampleVariant(variant)) return SAMPLE_WEIGHT_LB;
+  return TYPE_DEFAULT_LB[product.productType || product.product_type] ?? FALLBACK_LB;
+}
+
+/** THE CREATE-SIDE GUARD: keep a real positive weight, else fill the default.
+ *  Drop-in for the weight field in a create/productSet payload (returns lb). */
+export function resolveWeightLb(variant = {}, product = {}) {
+  const w = currentWeightLb(variant);
+  return (Number.isFinite(w) && w > 0) ? w : defaultWeightLb(variant, product);
+}
+
+/** 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 = {}) {
+  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/evidence/TK-11871/restore-map-20260917-1700/weight-guard.mjs.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.mjs.ORIG
new file mode 100644
index 00000000..8d8313d0
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.mjs.ORIG
@@ -0,0 +1,168 @@
+// weight-guard.mjs — TK-11414 (2026-09-10) prevention primitive.
+// Steve's rule: NO product may go ACTIVE with a missing/zero product WEIGHT
+// (zero-weight collapses orders into the lowest weight tier / free band and
+// 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 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.
+
+export const SAMPLE_WEIGHT_LB = 0.25;
+export const FALLBACK_LB = 2.0;
+
+// product_type -> sellable default weight (POUNDS)
+export const TYPE_DEFAULT_LB = {
+  'Wallcovering': 3.0, 'Wallcoverings': 3.0, 'Wallpaper': 3.0,
+  'Metallic Wallcovering': 3.0, 'Commercial Wallcovering': 3.0,
+  'Mural': 4.0,
+  'Fabric': 1.0, 'Commercial Fabric': 1.0, 'Commercial Drapery': 1.0,
+  'Trim': 0.5, 'Acoustic Panel': 6.0, 'Pillow': 1.5,
+  'Upholstered Walls/Panels': 6.0, 'Tin Ceiling Tile': 2.0,
+  'Hardware': 1.0, 'Furniture': 15.0, 'Memo Sample': 0.25,
+};
+
+const norm = t => String(t ?? '').trim().toLowerCase();
+
+/** Sample variant? (importers create `Sample` @ $4.25 + the real unit). */
+export function isSampleVariant(variant = {}) {
+  const label = norm(variant.title ?? variant.option1 ?? '');
+  const sku = norm(variant.sku);
+  if (label.includes('sample') || label.includes('memo')) return true;
+  if (sku.endsWith('-sample') || sku.includes('sample')) return true;
+  const p = Number(variant.price);
+  return Number.isFinite(p) && Math.abs(p - 4.25) < 0.01;
+}
+
+/** Current weight in lb, or 0 if missing/unparseable. Accepts variant.weight (REST),
+ *  variant.grams, or inventoryItem.measurement.weight.value (GraphQL). */
+export function currentWeightLb(variant = {}) {
+  const gql = variant?.inventoryItem?.measurement?.weight;
+  if (gql && gql.value != null) {
+    const v = Number(gql.value);
+    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) {
+    const v = Number(variant.weight);
+    const u = norm(variant.weight_unit || 'lb');
+    if (u.startsWith('kg')) return v * 2.20462;
+    if (u === 'g' || u.startsWith('gram')) return v / 453.59237;
+    if (u === 'oz') return v / 16;
+    return v; // lb
+  }
+  return 0;
+}
+
+export function hasZeroWeight(variant = {}) {
+  const w = currentWeightLb(variant);
+  return !Number.isFinite(w) || w <= 0;
+}
+
+/** The default weight (lb) to assign a variant that has none. */
+export function defaultWeightLb(variant = {}, product = {}) {
+  if (isSampleVariant(variant)) return SAMPLE_WEIGHT_LB;
+  return TYPE_DEFAULT_LB[product.productType || product.product_type] ?? FALLBACK_LB;
+}
+
+/** THE CREATE-SIDE GUARD: keep a real positive weight, else fill the default.
+ *  Drop-in for the weight field in a create/productSet payload (returns lb). */
+export function resolveWeightLb(variant = {}, product = {}) {
+  const w = currentWeightLb(variant);
+  return (Number.isFinite(w) && w > 0) ? w : defaultWeightLb(variant, product);
+}
+
+/** 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 = {}) {
+  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/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.mjs.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.mjs.ORIG
new file mode 100644
index 00000000..c037c957
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.mjs.ORIG
@@ -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/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.ts.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.ts.ORIG
new file mode 100644
index 00000000..0c6254b6
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.test.ts.ORIG
@@ -0,0 +1,96 @@
+// weight-guard.test.ts — TK-11539 negative test (CLAUDE.md TK-11431 amendment 3:
+// "a check ships with a negative test proving it goes red on an injected fault").
+//
+// The guard's job: no variant the shared import engine creates may carry a zero/missing
+// shipping weight. hasPositiveWeight() is the invariant; the tests prove it is RED on the
+// pre-fix (un-guarded) payload and GREEN on the guarded payload, and that the guard resolves
+// sane, positive, per-product-type weights.
+
+import {
+  inventoryItemWithWeight,
+  hasPositiveWeight,
+  sellableWeightLb,
+  sampleWeightLb,
+  SAMPLE_WEIGHT_LB,
+  FALLBACK_LB,
+  TYPE_DEFAULT_LB,
+} from '../lib/weight-guard';
+
+describe('TK-11539 weight guard — weight resolution', () => {
+  it('sellable Wallcovering resolves to the TK-11414 backfill default (3.0 lb)', () => {
+    expect(sellableWeightLb('Wallcovering')).toBe(TYPE_DEFAULT_LB['Wallcovering']);
+    expect(sellableWeightLb('Wallcovering')).toBe(3.0);
+  });
+
+  it('sellable resolution is case-insensitive', () => {
+    expect(sellableWeightLb('wallcovering')).toBe(3.0);
+    expect(sellableWeightLb('  FABRIC ')).toBe(1.0);
+  });
+
+  it('unknown/blank product_type falls back to FALLBACK_LB (never zero)', () => {
+    expect(sellableWeightLb('Nonexistent Type')).toBe(FALLBACK_LB);
+    expect(sellableWeightLb('')).toBe(FALLBACK_LB);
+    expect(sellableWeightLb(undefined)).toBe(FALLBACK_LB);
+    expect(sellableWeightLb(null)).toBe(FALLBACK_LB);
+    expect(FALLBACK_LB).toBeGreaterThan(0);
+  });
+
+  it('sample weight is the approved 0.25 lb', () => {
+    expect(sampleWeightLb()).toBe(SAMPLE_WEIGHT_LB);
+    expect(sampleWeightLb()).toBe(0.25);
+  });
+});
+
+describe('TK-11539 weight guard — inventoryItemWithWeight builder', () => {
+  it('sellable payload keeps base fields AND carries a positive weight in POUNDS', () => {
+    const out = inventoryItemWithWeight(
+      { sku: 'DWXX-1-Roll', tracked: true },
+      { role: 'sellable', productType: 'Wallcovering' }
+    );
+    expect(out.sku).toBe('DWXX-1-Roll');
+    expect(out.tracked).toBe(true);
+    expect(out.measurement.weight.unit).toBe('POUNDS');
+    expect(out.measurement.weight.value).toBe(3.0);
+    expect(hasPositiveWeight(out)).toBe(true);
+  });
+
+  it('sample payload keeps base fields AND carries 0.25 lb', () => {
+    const out = inventoryItemWithWeight(
+      { sku: 'DWXX-1-Sample', tracked: false },
+      { role: 'sample', productType: 'Wallcovering' }
+    );
+    expect(out.sku).toBe('DWXX-1-Sample');
+    expect(out.tracked).toBe(false);
+    expect(out.measurement.weight.value).toBe(0.25);
+    expect(hasPositiveWeight(out)).toBe(true);
+  });
+
+  it('guarantees a positive weight even for an unknown product_type', () => {
+    const out = inventoryItemWithWeight(
+      { sku: 'DWXX-2-Roll', tracked: true },
+      { role: 'sellable', productType: 'Totally Unknown' }
+    );
+    expect(out.measurement.weight.value).toBe(FALLBACK_LB);
+    expect(hasPositiveWeight(out)).toBe(true);
+  });
+});
+
+describe('TK-11539 weight guard — NEGATIVE test (proves the invariant has teeth)', () => {
+  it('REDDENS on the PRE-FIX payload (the exact zero-weight fault this guard removes)', () => {
+    // This is precisely what addSampleVariant wrote before TK-11539: no measurement.
+    const preFixUnguardedPayload = { sku: 'DWXX-1-Roll', tracked: true };
+    expect(hasPositiveWeight(preFixUnguardedPayload)).toBe(false);
+  });
+
+  it('REDDENS on an injected zero / negative / non-finite weight', () => {
+    expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: 0, unit: 'POUNDS' } } })).toBe(false);
+    expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: -3, unit: 'POUNDS' } } })).toBe(false);
+    expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: NaN, unit: 'POUNDS' } } })).toBe(false);
+    expect(hasPositiveWeight({ sku: 'x', measurement: { weight: {} } })).toBe(false);
+  });
+
+  it('GREENS on the guarded payload — closing the loop against the pre-fix fault', () => {
+    const guarded = inventoryItemWithWeight({ sku: 'DWXX-1-Roll', tracked: true }, { role: 'sellable', productType: 'Wallcovering' });
+    expect(hasPositiveWeight(guarded)).toBe(true);
+  });
+});
diff --git a/evidence/TK-11871/restore-map-20260917-1700/weight-guard.ts.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.ts.ORIG
new file mode 100644
index 00000000..6f46a611
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight-guard.ts.ORIG
@@ -0,0 +1,74 @@
+// weight-guard.ts — TK-11539 (2026-09-12) CREATE-PATH weight guard for the shared
+// ImportNewSkufromURL engine (Carnegie + every vendor routed through /api/create-product).
+//
+// Prevents a recurrence of the TK-11414 zero-weight gap: createOrGetProduct/addSampleVariant
+// created variants with inventoryItem:{sku,tracked} but NEVER set measurement.weight, so every
+// imported product shipped at ZERO weight. Zero weight collapses orders into the lowest
+// freight tier / the free-shipping band and mis-costs DW shipping (Steve's rule: NO product
+// may go ACTIVE with a missing/zero product WEIGHT).
+//
+// Values mirror the canonical backfill primitive:
+//   ~/Projects/designerwallcoverings/scripts/lib/weight-guard.mjs
+// (SAMPLE_WEIGHT_LB, FALLBACK_LB, TYPE_DEFAULT_LB). Keep this in sync with that file and the
+// approved TK-11414 backfill. Unit is always POUNDS (Shopify WeightUnit enum).
+
+export const SAMPLE_WEIGHT_LB = 0.25;
+export const FALLBACK_LB = 2.0;
+
+// product_type -> sellable default weight (POUNDS). Mirrors weight-guard.mjs.
+export const TYPE_DEFAULT_LB: Record<string, number> = {
+  'Wallcovering': 3.0, 'Wallcoverings': 3.0, 'Wallpaper': 3.0,
+  'Metallic Wallcovering': 3.0, 'Commercial Wallcovering': 3.0,
+  'Mural': 4.0,
+  'Fabric': 1.0, 'Commercial Fabric': 1.0, 'Commercial Drapery': 1.0,
+  'Trim': 0.5, 'Acoustic Panel': 6.0, 'Pillow': 1.5,
+  'Upholstered Walls/Panels': 6.0, 'Tin Ceiling Tile': 2.0,
+  'Hardware': 1.0, 'Furniture': 15.0, 'Memo Sample': 0.25,
+};
+
+export type WeightUnit = 'POUNDS';
+
+export interface WeightMeasurement {
+  measurement: { weight: { value: number; unit: WeightUnit } };
+}
+
+const norm = (t?: string | null): string => String(t ?? '').trim().toLowerCase();
+
+/** Sellable default weight (lb) for a product_type; unknown/blank -> FALLBACK_LB. Case-insensitive. */
+export function sellableWeightLb(productType?: string | null): number {
+  const key = Object.keys(TYPE_DEFAULT_LB).find(k => norm(k) === norm(productType));
+  const w = key ? TYPE_DEFAULT_LB[key] : FALLBACK_LB;
+  return (Number.isFinite(w) && w > 0) ? w : FALLBACK_LB;
+}
+
+/** Sample-variant weight (lb) — always the approved 0.25 lb. */
+export function sampleWeightLb(): number {
+  return SAMPLE_WEIGHT_LB;
+}
+
+/**
+ * THE CREATE-SIDE GUARD. Merge a positive measurement.weight into a
+ * ProductVariantsBulkInput.inventoryItem input so no variant is ever created zero-weight.
+ *   role 'sample'   -> SAMPLE_WEIGHT_LB (0.25 lb)
+ *   role 'sellable' -> per-productType default (FALLBACK_LB when unknown)
+ * The returned inventoryItem ALWAYS carries weight.value > 0.
+ * Valid on ProductVariantsBulkInput.inventoryItem (InventoryItemInput.measurement) in the
+ * Shopify Admin API 2024-07+.
+ */
+export function inventoryItemWithWeight<T extends object>(
+  base: T,
+  opts: { role: 'sellable' | 'sample'; productType?: string | null }
+): T & WeightMeasurement {
+  const value = opts.role === 'sample' ? sampleWeightLb() : sellableWeightLb(opts.productType);
+  return { ...base, measurement: { weight: { value, unit: 'POUNDS' } } };
+}
+
+/**
+ * Invariant used by the negative test and callable as a runtime assert: a variant's
+ * inventoryItem input must carry a finite, positive measurement.weight. Returns false for
+ * the pre-fix payload (no measurement), which is exactly the fault the guard removes.
+ */
+export function hasPositiveWeight(inventoryItemInput: any): boolean {
+  const v = inventoryItemInput?.measurement?.weight?.value;
+  return typeof v === 'number' && Number.isFinite(v) && v > 0;
+}
diff --git a/evidence/TK-11871/restore-map-20260917-1700/weight_guard.py.ORIG b/evidence/TK-11871/restore-map-20260917-1700/weight_guard.py.ORIG
new file mode 100644
index 00000000..da259a88
--- /dev/null
+++ b/evidence/TK-11871/restore-map-20260917-1700/weight_guard.py.ORIG
@@ -0,0 +1,299 @@
+#!/usr/bin/env python3
+"""weight_guard.py — TK-11471 PYTHON twin of lib/weight-guard.mjs (same directory).
+
+Steve's rule (TK-11414): NO product may go ACTIVE with a missing/zero product WEIGHT
+(zero weight collapses orders into the lowest weight tier / free-shipping band and
+mis-costs DW freight).
+
+Why a Python twin exists at all: the .mjs guard covers the JS onboarders, but
+dwpw-grs-migrate.py — the job that actually created 80 zero-weight ACTIVE variants
+on 2026-09-11 — is PYTHON and cannot import it. A .mjs-only guard is not a guard
+for this repo. Constants are kept BYTE-IDENTICAL to the .mjs so the two
+implementations cannot drift into disagreeing about what a compliant weight is;
+test_weight_guard.py asserts that mechanically against the .mjs source.
+
+Two call sites:
+  1. create payload:  weight = resolve_weight_lb(variant, product)   (unit POUNDS)
+  2. before activate: r = heal_and_verify_weights(gql, gid, product)
+                      if not r["ok"]: HOLD as draft (never flip ACTIVE at zero weight)
+
+SAMPLES COUNT. dw-active-weight-canary FAILs on ANY zero-weight ACTIVE variant —
+its last live run split the offenders 43 sample / 40 sellable. The .mjs's
+zeroWeightBlockers() filters samples OUT and is DEPRECATED there; that bug is
+deliberately NOT ported. all_zero_weight_variants() is the only population view here.
+"""
+import math
+
+SAMPLE_WEIGHT_LB = 0.25
+FALLBACK_LB = 2.0
+
+# product_type -> sellable default weight (POUNDS). Identical to the .mjs table.
+TYPE_DEFAULT_LB = {
+    'Wallcovering': 3.0, 'Wallcoverings': 3.0, 'Wallpaper': 3.0,
+    'Metallic Wallcovering': 3.0, 'Commercial Wallcovering': 3.0,
+    'Mural': 4.0,
+    'Fabric': 1.0, 'Commercial Fabric': 1.0, 'Commercial Drapery': 1.0,
+    'Trim': 0.5, 'Acoustic Panel': 6.0, 'Pillow': 1.5,
+    'Upholstered Walls/Panels': 6.0, 'Tin Ceiling Tile': 2.0,
+    'Hardware': 1.0, 'Furniture': 15.0, 'Memo Sample': 0.25,
+}
+
+NAN = float('nan')
+
+
+def _norm(t):
+    return ('' if t is None else str(t)).strip().lower()
+
+
+def _num(v):
+    """float(v), or NaN when unparseable — mirrors JS Number() -> NaN. NaN is
+    treated as zero-weight downstream, so a junk value FAILS SAFE."""
+    try:
+        f = float(v)
+    except (TypeError, ValueError):
+        return NAN
+    return f
+
+
+def _d(x):
+    return x if isinstance(x, dict) else {}
+
+
+def _gql_weight(variant):
+    """inventoryItem.measurement.weight from a GraphQL node (camel or snake key)."""
+    iv = _d(variant).get('inventoryItem')
+    if iv is None:
+        iv = _d(variant).get('inventory_item')
+    return _d(_d(iv).get('measurement')).get('weight')
+
+
+def is_sample_variant(variant=None):
+    """Sample variant? (importers create `Sample` @ $4.25 + the real unit)."""
+    v = _d(variant)
+    label = _norm(v.get('title') if v.get('title') is not None else v.get('option1'))
+    sku = _norm(v.get('sku'))
+    if 'sample' in label or 'memo' in label:
+        return True
+    if sku.endswith('-sample') or 'sample' in sku:
+        return True
+    p = _num(v.get('price'))
+    return math.isfinite(p) and abs(p - 4.25) < 0.01
+
+
+def weight_field_present(variant=None):
+    """Did the payload we were handed CARRY a weight field at all?
+
+    TK-11431 amendment 1 — an UNMEASURED input is never a PASS. A response that
+    simply never contained weight must not be read as 'weight is zero' by luck,
+    nor as 'fine'. has_zero_weight() flags both, but a caller that needs to
+    distinguish 'measured 0' from 'never measured' asks here.
+    """
+    v = _d(variant)
+    if _d(_gql_weight(v)).get('value') is not None:
+        return True
+    return v.get('grams') is not None or v.get('weight') is not None
+
+
+def current_weight_lb(variant=None):
+    """Current weight in POUNDS, or 0 if missing. Accepts variant.weight (+weight_unit,
+    REST), variant.grams, or inventoryItem.measurement.weight{value,unit} (GraphQL)."""
+    v = _d(variant)
+    g = _d(_gql_weight(v))
+    if g.get('value') is not None:
+        val = _num(g.get('value'))
+        u = _norm(g.get('unit'))                      # Shopify WeightUnit enum
+        if u == 'kilograms' or u.startswith('kg'):
+            return val * 2.20462
+        if u == 'grams' or u == 'g':
+            return val / 453.59237
+        if u == 'ounces' or u == 'oz':
+            return val / 16
+        return val                                    # POUNDS (unit-less => assume lb)
+    if v.get('grams') is not None:
+        return _num(v.get('grams')) / 453.59237
+    if v.get('weight') is not None:
+        val = _num(v.get('weight'))
+        u = _norm(v.get('weight_unit') or 'lb')
+        if u.startswith('kg'):
+            return val * 2.20462
+        if u == 'g' or u.startswith('gram'):
+            return val / 453.59237
+        if u == 'oz':
+            return val / 16
+        return val                                    # lb
+    return 0.0
+
+
+def has_zero_weight(variant=None):
+    w = current_weight_lb(variant)
+    return (not math.isfinite(w)) or w <= 0
+
+
+def default_weight_lb(variant=None, product=None):
+    """The default weight (lb) to assign a variant that has none."""
+    if is_sample_variant(variant):
+        return SAMPLE_WEIGHT_LB
+    p = _d(product)
+    ptype = p.get('productType') if p.get('productType') is not None else p.get('product_type')
+    return TYPE_DEFAULT_LB.get(ptype, FALLBACK_LB)
+
+
+def resolve_weight_lb(variant=None, product=None):
+    """THE CREATE-SIDE GUARD: keep a real positive weight, else fill the default.
+    Drop-in for the REST variant `weight` field (returns POUNDS; pair with
+    weight_unit='lb')."""
+    w = current_weight_lb(variant)
+    return w if (math.isfinite(w) and w > 0) else default_weight_lb(variant, product)
+
+
+def variants_of(product=None):
+    """Normalize variants out of either a GraphQL connection or a plain REST array."""
+    p = _d(product)
+    edges = _d(p.get('variants')).get('edges')
+    if isinstance(edges, list):
+        return [e.get('node') for e in edges if isinstance(e, dict)]
+    v = p.get('variants')
+    return v if isinstance(v, list) else []
+
+
+def all_zero_weight_variants(product=None):
+    """THE ACTIVATE-SIDE GUARD: EVERY zero-weight variant, SAMPLE INCLUDED.
+    Matches dw-active-weight-canary exactly. Non-empty => DO NOT flip ACTIVE."""
+    return [v for v in variants_of(product) if has_zero_weight(v)]
+
+
+def unmeasured_variants(product=None):
+    """Variants whose payload carried NO weight field at all — the response never
+    measured them. Returned separately so a caller can report NOT-MEASURED rather
+    than claiming a clean zero reading."""
+    return [v for v in variants_of(product) if not weight_field_present(v)]
+
+
+def variant_label(variant=None):
+    v = _d(variant)
+    return v.get('sku') or v.get('title') or v.get('id') or '?'
+
+
+# ---------------------------------------------------------------- activate gate
+# The re-query an activate site MUST run so the guard MEASURES something. A product
+# query that omits inventoryItem{measurement{weight}} makes every variant look
+# zero-weight to current_weight_lb, and one that omits productType silently defaults
+# every heal to FALLBACK_LB. Both fields are required.
+WEIGHT_REQUERY = (
+    "query($id:ID!){ product(id:$id){ productType variants(first:100){edges{node{ "
+    "id sku title price inventoryItem{ id measurement{ weight{ value unit } } } }}} } }"
+)
+
+M_WEIGHT_SET = (
+    "mutation($id:ID!,$w:Float!){ inventoryItemUpdate(id:$id, "
+    "input:{measurement:{weight:{value:$w, unit:POUNDS}}}){ userErrors{message} } }"
+)
+
+
+def response_carries_weight_field(product=None):
+    """Did the re-query response ACTUALLY contain the weight field we asked for?
+
+    A gate that reads a response which never carried weight silently passes
+    everything (or, here, fails everything) for the wrong reason — the false-green
+    class CLAUDE.md warns about. Every variant node must carry an `inventoryItem`
+    key holding a `measurement` key. `measurement.weight = null` is a legitimate
+    'no weight set' reading and is NOT a shape failure.
+    """
+    vs = variants_of(product)
+    if not vs:
+        return False
+    for v in vs:
+        d = _d(v)
+        iv = d.get('inventoryItem', d.get('inventory_item'))
+        if not isinstance(iv, dict) or 'measurement' not in iv:
+            return False
+    return True
+
+
+def heal_and_verify_weights(gql, product_gid, product=None, requery=None, mutation=None,
+                            rest_heal=None):
+    """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 default_weight_lb() in POUNDS
+      2. the product is RE-READ and re-checked — the mutation's own 200 is not evidence
+      3. ok is False  => caller must HOLD the product as draft and name `weight>0`
+
+    Idempotent and a NO-OP (zero network calls) when every weight is already positive.
+    Fails CLOSED: an unreadable re-query, a response that never carried the weight
+    field, a missing inventoryItem id, or a userError all yield ok=False.
+
+    gql        -- the call site's own gql(query, variables) -> parsed JSON
+    rest_heal  -- optional fallback heal fn (variant_dict, lb) -> True/False, used when
+                  the GraphQL inventoryItemUpdate path is unavailable (e.g. a token
+                  without write_inventory). Verification stays GraphQL-side either way.
+    """
+    requery = requery or WEIGHT_REQUERY
+    mutation = mutation or M_WEIGHT_SET
+    errs, healed, still_zero = [], [], []
+    heal_failures = 0
+    p = _d(product)
+    product_type = p.get('productType') if p.get('productType') is not None else p.get('product_type')
+
+    zero = all_zero_weight_variants(p)
+    if not zero:
+        return {"ok": True, "healed": healed, "stillZero": [], "errs": errs}   # no-op
+
+    for v in zero:
+        label = variant_label(v)
+        lb = default_weight_lb(v, {"productType": product_type})
+        iid = _d(_d(v).get('inventoryItem') or _d(v).get('inventory_item')).get('id')
+        wrote = False
+        if iid:
+            try:
+                r = gql(mutation, {"id": iid, "w": lb})
+                ue = (((r or {}).get('data') or {}).get('inventoryItemUpdate') or {}).get('userErrors') or []
+                top = (r or {}).get('errors') or []
+                for e in ue:
+                    errs.append("weight:%s:%s" % (label, e.get('message')))
+                for e in top:
+                    errs.append("weight:%s:%s" % (label, str(e.get('message'))[:80]))
+                wrote = not ue and not top
+            except Exception as e:                       # noqa: BLE001 - fail closed
+                errs.append("weight:%s:%s" % (label, str(e)[:80]))
+        else:
+            errs.append("weight:no-inventory-item:%s" % label)
+
+        if not wrote and rest_heal is not None:
+            try:
+                wrote = bool(rest_heal(v, lb))
+            except Exception as e:                       # noqa: BLE001 - fail closed
+                errs.append("weight:rest:%s:%s" % (label, str(e)[:80]))
+
+        if wrote:
+            healed.append({"sku": label, "lb": lb})
+        else:
+            heal_failures += 1
+
+    # RE-VERIFY against the live record. Never trust the write.
+    fresh = None
+    try:
+        resp = gql(requery, {"id": product_gid})
+        if (resp or {}).get('errors'):
+            errs.append("weight:reverify:%s" % str(resp['errors'])[:80])
+        fresh = ((resp or {}).get('data') or {}).get('product')
+    except Exception as e:                               # noqa: BLE001 - fail closed
+        errs.append("weight:reverify:%s" % str(e)[:80])
+    if not fresh:
+        errs.append("weight:reverify-failed")
+        return {"ok": False, "healed": healed, "stillZero": [], "errs": errs}
+    if not response_carries_weight_field(fresh):
+        # NOT MEASURED. The re-query came back without the field it asked for, so a
+        # clean-looking read proves nothing. Never a PASS.
+        errs.append("weight:reverify-response-carried-no-weight-field")
+        return {"ok": False, "healed": healed, "stillZero": [], "errs": errs}
+
+    still_zero = [variant_label(v) for v in all_zero_weight_variants(fresh)]
+    # FAIL CLOSED on an UNHEALED variant even when the re-verify comes back clean: a
+    # variant we could not write is UNMEASURED with respect to our own action, and 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": (not still_zero) and heal_failures == 0,
+            "healed": healed, "stillZero": still_zero, "errs": errs}

← fefe129c ticket-runner: capture rc before date cmdsub — failures no l  ·  back to Ticket System  ·  tk: evidence guard on done (open gated memo / no logged work dee8eacd →