[object Object]

← back to Designerwallcoverings

auto-data-snapshot: 2026-09-22T11:43:24 (10 data files) — OWNERSHIP.md data/orphan-cleanup-20260716/latest.json package-lock.json package.json scripts/innovations-image-audit/audit.mjs.pre-pg-decouple

f5370f4df37377e48984b9ec0cc0514172d62397 · 2026-09-22 11:43:33 -0700 · auto-commit-fleet

Files touched

Diff

commit f5370f4df37377e48984b9ec0cc0514172d62397
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date:   Tue Sep 22 11:43:33 2026 -0700

    auto-data-snapshot: 2026-09-22T11:43:24 (10 data files) — OWNERSHIP.md data/orphan-cleanup-20260716/latest.json package-lock.json package.json scripts/innovations-image-audit/audit.mjs.pre-pg-decouple
---
 OWNERSHIP.md                                       |  39 ++++
 data/orphan-cleanup-20260716/latest.json           |   2 +-
 package-lock.json                                  | 149 ++++++++++++-
 package.json                                       |   3 +-
 .../audit.mjs.pre-pg-decouple                      | 127 +++++++++++
 .../archive-discontinued.mjs.pre-pg-decouple       |  83 +++++++
 .../reconcile.mjs.pre-pg-decouple                  |  88 ++++++++
 .../lib/inventory-stamp-guard.mjs.pre-shared-lift  |  80 +++++++
 .../build-titles.mjs.pre-pg-decouple               | 248 +++++++++++++++++++++
 .../scrub-true-sku.mjs.pre-pg-decouple             | 239 ++++++++++++++++++++
 10 files changed, 1055 insertions(+), 3 deletions(-)

diff --git a/OWNERSHIP.md b/OWNERSHIP.md
new file mode 100644
index 0000000..cc23907
--- /dev/null
+++ b/OWNERSHIP.md
@@ -0,0 +1,39 @@
+# OWNERSHIP — designerwallcoverings (lowercase)
+
+_Authoritative boundary doc. Steve approved "Option B — keep separate + decouple" on 2026-09-22 (analysis memo `~/.claude/yolo-queue/pending-approval/dw-two-repos-consolidation-analysis-20260922.md`). This file + its twin in `~/Projects/Designer-Wallcoverings/OWNERSHIP.md` are the source of truth for which repo owns what. There are TWO DW repos on purpose — they are NOT duplicates, NOT to be merged._
+
+## What THIS repo is
+`~/Projects/designerwallcoverings` — pkg `designerwallcoverings-ai` v0.1.17. Born as the **designerwallcoverings.ai room-render landing** (`server.js`: upload a room photo → Gemini Vision palette/style read → score the local catalog snapshot `data/products.json` → hand checkout back to designerwallcoverings.com). Deployed to **Kamatera** via `ecosystem.config.js` (pm2 app `designerwallcoverings.ai`, PORT 9925, cwd `/root/Projects/designerwallcoverings`). The "-ai" in the pkg name is literal (the `.ai` landing product) — it is NOT "the AI version of the catalog repo". 2.5 GB, 1 nested repo, local-only (no git remote). ~18 `com.steve.*` launchd jobs run its scripts.
+
+## This repo OWNS (canonical)
+- **The designerwallcoverings.ai room-render landing** (`server.js`, its Kamatera pm2:9925 deploy).
+- **NEW vendor onboarding** — the 2026-Q3 onboarders under `scripts/`: sanderson-onboard, greenland (via plist), stroheim-onboard, osborne-onboard, china-seas-onboard, artmura-onboard, coleson-onboard, fallingstar-onboard, muralsource-onboard, stout-onboard, maharam-onboard, knoll-onboard, tres-tintas-onboard, activate-1838, etc.
+- **Catalog DATA-QUALITY tooling** — `scripts/price-sheets/` (schu-watch), dedup (`dedup-archive-all.js`, `dedup-verify-all.js`), `orphan-publish-guard.mjs`, GMC, roll-scale, showroom-lines.
+- Resolves real Shopify/DB creds from the canonical `~/Projects/secrets-manager/.env` (uses `SHOPIFY_FULL_ACCESS_TOKEN`); its own `.env` holds only `SHOPIFY_DRAFT_TOKEN`.
+
+## The OTHER repo (`~/Projects/Designer-Wallcoverings`, capital, pkg `designer-wallcoverings`)
+Owns: the **Shopify storefront/theme layer**, the **hourly product-upload cadence engine** (`shopify/scripts/cadence/`), the DW-* nested monorepo stack, and (historically) the **canonical shared guard libs** (now mastered in `~/Projects/_shared/dw-guards/`).
+
+## Shared-vendor ownership (the diverged forks)
+Both repos historically carried code for anna-french, cole-son/coleson, romo, kravet, china-seas, artmura. Going-forward SINGLE owner per vendor is drafted for Steve in a gated memo (freeze/repoint of the losing fork NOT yet executed). Until ratified, do NOT add a NEW second fork of any of these in the other repo.
+
+### Ratified 2026-09-22 (Steve fired the vendor-fork memo)
+- **romo → THIS repo (lowercase) canonical** for roll creation/verify (`com.steve.gimmersta-romo-rolls`, `com.steve.romo-roll-verify`, `build-romo-rolls.mjs`). Capital's orphan `romo-lookup.js` / `romo-drilldown.js` were frozen to its `_superseded/`. Functional split: capital RETAINS romo roll PRICING (`romo-add-roll-pricing.js` + `data/romo-cost-backfill/`) — not frozen.
+- **artmura → THIS repo canonical** (`scripts/artmura-onboard/`). Capital's stale `artmura-title-fix.js` was frozen.
+- **cole-son → FUNCTIONAL SPLIT, both kept:** THIS repo owns cole-son **2026 pricing / pattern-map** (`scripts/cole-son-reprice-2026.js`, `-price-audit.mjs`, `-apply-map.js`, `-apply-pattern-map.js`, `-reconcile-pattern.js`, `cole-son-map-verify/`); capital owns cole-son/coleson **variant/onboarding/metafields**. Different fields — do NOT freeze either.
+- **kravet → FUNCTIONAL SPLIT, both kept (both live jobs):** THIS repo owns kravet **spec/onboarding** (`com.steve.kravet-spec-backfill` + `scripts/price-sheets/`); capital owns kravet **price/MAP watch** (`com.steve.kravet-price-monitor`, `com.steve.kravet-overnight-summary`). Do NOT freeze either.
+- **anna-french → FUNCTIONAL SPLIT, both kept (NOT frozen):** THIS repo owns live-remediation `scripts/anna-french-onboard/` (TK-11076 colorway/AI-tag correction + rollback) + `scripts/anna-french-rolls/` (roll build/reprice); capital owns the scrape/enrich/cadence-dwsku pipeline. Verified 2026-09-22: capital's in-repo `com.steve.anna-french-roll-drain.plist` is NOT loaded. Distinct functions — no freeze.
+- **china-seas → both kept (NOT frozen):** THIS repo owns live remediation `scripts/china-seas-onboard/` (TK-11076); capital's china-seas files are SPENT June-2026 one-shots (type reconcile + commercial-revert, completed). Temporal/functional split — no freeze.
+
+## Shared guard libs — canonical home is `~/Projects/_shared/dw-guards/`
+As of the 2026-09-22 decouple:
+- `~/Projects/_shared/dw-guards/{validate-before-activate.js, internal-guard.js, inventory-stamp-guard.mjs}` are the **documented canonical masters**.
+- This repo's 3 go-live scripts (`stroheim-onboard/go-live.mjs`, `muralsource-onboard/go-live.mjs`, `stout-onboard/go-live.mjs`) import `validate-before-activate` by absolute path into the capital repo; that path is now a re-export shim → the `_shared` master, so they keep working unchanged.
+- This repo keeps its OWN `scripts/lib/inventory-stamp-guard.mjs` copy (part of the deliberate drift-checked 5-repo copy set guarded by capital's `predicate-proof.mjs`). Full consolidation behind `_shared` is a gated follow-up.
+
+## Cross-repo coupling status (what the decouple fixed)
+- **pg**: 5 scripts (innovations-mfr-reconcile/{reconcile,archive-discontinued}.mjs, innovations-image-audit/audit.mjs, true-sku-leak-scrub/scrub-true-sku.mjs, title-campaign/build-titles.mjs) used to fall back to capital's `node_modules/pg`. **FIXED** — this repo now has its own `pg` dependency and the capital-path fallback was removed.
+- guard-lib import (validate-before-activate) now resolves through the `_shared` master via capital's shim.
+- 1 script still reads capital's `.env` — noted for a future decouple (point at `secrets-manager/.env`).
+
+**Hard rails:** both repos have automated writers committing ~hourly — never run `git add/commit/reset/checkout` by hand in either. `dw_unified` + Shopify writes are canonical/customer-facing (gated). Live store = `designer-laboratory-sandbox.myshopify.com` (API 2024-10). Do NOT touch the launchd plists or the Kamatera pm2:9925 deploy without Steve.
diff --git a/data/orphan-cleanup-20260716/latest.json b/data/orphan-cleanup-20260716/latest.json
index 4577cf0..bfeb9da 100644
--- a/data/orphan-cleanup-20260716/latest.json
+++ b/data/orphan-cleanup-20260716/latest.json
@@ -1,5 +1,5 @@
 {
-  "ts": "2026-09-22T13:46:06.377Z",
+  "ts": "2026-09-22T18:35:13.721Z",
   "verdict": "PASS",
   "ok": true,
   "mode": "detect",
diff --git a/package-lock.json b/package-lock.json
index 79d0d70..4681f63 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,8 @@
       "version": "0.1.17",
       "dependencies": {
         "express": "^4.19.2",
-        "multer": "^2.0.1"
+        "multer": "^2.0.1",
+        "pg": "^8.23.0"
       },
       "engines": {
         "node": ">=20"
@@ -640,6 +641,134 @@
       "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
       "license": "MIT"
     },
+    "node_modules/pg": {
+      "version": "8.23.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz",
+      "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.14.0",
+        "pg-pool": "^3.14.0",
+        "pg-protocol": "^1.16.0",
+        "pg-types": "2.2.0",
+        "pgpass": "1.0.5"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.4.0"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+      "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+      "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+      "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
+      "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+      "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -855,6 +984,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
     "node_modules/statuses": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -941,6 +1079,15 @@
       "engines": {
         "node": ">= 0.8"
       }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 2047e73..61f6fa4 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
   },
   "dependencies": {
     "express": "^4.19.2",
-    "multer": "^2.0.1"
+    "multer": "^2.0.1",
+    "pg": "^8.23.0"
   }
 }
diff --git a/scripts/innovations-image-audit/audit.mjs.pre-pg-decouple b/scripts/innovations-image-audit/audit.mjs.pre-pg-decouple
new file mode 100644
index 0000000..b55599e
--- /dev/null
+++ b/scripts/innovations-image-audit/audit.mjs.pre-pg-decouple
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+/**
+ * Innovations USA primary-image quality audit.
+ * Flags product PRIMARY images that are (a) multi-item compositions or
+ * (b) room scenes where the wallcovering is barely visible — both bad primary
+ * images for a wallcovering listing (the pattern should be shown clearly).
+ *
+ * READ-ONLY: produces a review list (JSON+CSV+HTML). Never edits a product.
+ * Uses Gemini 2.0 Flash (~$0.0006/image) per DW image-analysis rule.
+ *
+ * Usage: node audit.mjs [--limit N]
+ */
+import { readFileSync, writeFileSync, existsSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i > -1 ? parseInt(process.argv[i + 1]) : 0; })();
+const CONCURRENCY = 4;
+const PRICE_PER_IMG = 0.0006;
+
+const GEMINI_KEY = (() => {
+  if (process.env.GEMINI_API_KEY) return process.env.GEMINI_API_KEY;
+  const env = readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
+  const m = env.match(/^GEMINI_API_KEY=(.+)$/m);
+  return m ? m[1].trim().replace(/^["']|["']$/g, '') : null;
+})();
+const MODEL = 'gemini-2.5-flash';
+const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_KEY}`;
+
+const PROMPT = `You are auditing the PRIMARY product image of a WALLCOVERING (wallpaper) listing.
+Classify the image into exactly one verdict:
+- "CLEAN": a single, clear view of the wallcovering itself — a swatch, flat sample, close-up of the pattern/texture, or a wall mostly filled with the covering. This is a GOOD primary image.
+- "MULTI_ITEM": the image clearly shows MULTIPLE distinct items/products — e.g. several rolls or swatches together, a collage/grid of variants, a styled flat-lay with assorted objects, multiple framed samples. The shopper can't tell which single product this is.
+- "ROOM_SPARSE": a room / interior / styled scene where the wallcovering covers only a SMALL portion of the frame or is barely visible (furniture, decor, people, or empty space dominate).
+Return ONLY minified JSON: {"verdict":"CLEAN|MULTI_ITEM|ROOM_SPARSE","confidence":0.0-1.0,"reason":"<8 words max>"}`;
+
+async function fetchImageB64(url) {
+  const res = await fetch(url, { redirect: 'follow' });
+  if (!res.ok) throw new Error('img ' + res.status);
+  const ct = res.headers.get('content-type') || 'image/jpeg';
+  const buf = Buffer.from(await res.arrayBuffer());
+  return { b64: buf.toString('base64'), mime: ct.split(';')[0] };
+}
+
+async function classify(url) {
+  const { b64, mime } = await fetchImageB64(url);
+  // LOCAL-FIRST cost-saver (2026-07-03): free ollama qwen2.5vl before paid Gemini. ENRICH_GEMINI_ONLY=1 forces Gemini.
+  if (process.env.ENRICH_GEMINI_ONLY !== '1') {
+    try {
+      const lr = await fetch('http://127.0.0.1:11434/api/generate', {
+        method: 'POST', headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ model: process.env.OLLAMA_VISION_MODEL || 'qwen2.5vl:7b', prompt: PROMPT,
+          images: [b64], format: 'json', stream: false, options: { temperature: 0 } }),
+        signal: AbortSignal.timeout(90000) });
+      if (lr.ok) { const lj = await lr.json(); const mm = (lj.response || '').match(/\{[\s\S]*\}/); if (mm) return JSON.parse(mm[0]); }
+    } catch (_) { /* fall through to Gemini */ }
+  }
+  const body = { contents: [{ parts: [{ text: PROMPT }, { inline_data: { mime_type: mime, data: b64 } }] }],
+    generationConfig: { temperature: 0, maxOutputTokens: 300, responseMimeType: 'application/json', thinkingConfig: { thinkingBudget: 0 } } };
+  const res = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+  if (!res.ok) throw new Error('gemini ' + res.status + ' ' + (await res.text()).slice(0, 120));
+  const j = await res.json();
+  const txt = (j?.candidates?.[0]?.content?.parts || []).map(p => p.text || '').join('');
+  const m = txt.match(/\{[\s\S]*\}/);
+  if (!m) throw new Error('no json: ' + txt.slice(0, 80));
+  return JSON.parse(m[0]);
+}
+
+function loadProducts() {
+  const require = createRequire(import.meta.url);
+  let pg; try { pg = require('pg'); } catch { pg = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/pg'); }
+  return pg;
+}
+
+async function main() {
+  if (!GEMINI_KEY) { console.error('No GEMINI_API_KEY'); process.exit(1); }
+  const pg = loadProducts();
+  const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp' });
+  let { rows } = await pool.query(
+    `SELECT handle, dw_sku, title, image_url FROM shopify_products
+     WHERE lower(status)='active' AND vendor ILIKE '%innovation%' AND image_url IS NOT NULL AND image_url<>''
+     ORDER BY handle`);
+  await pool.end();
+  if (LIMIT) rows = rows.slice(0, LIMIT);
+  console.log(`Auditing ${rows.length} Innovations images (concurrency ${CONCURRENCY})…\n`);
+
+  const results = [];
+  let done = 0, errs = 0;
+  for (let i = 0; i < rows.length; i += CONCURRENCY) {
+    const slice = rows.slice(i, i + CONCURRENCY);
+    await Promise.all(slice.map(async (p) => {
+      try {
+        const v = await classify(p.image_url);
+        results.push({ ...p, ...v });
+        if (v.verdict !== 'CLEAN') console.log(`  ⚑ ${v.verdict} (${v.confidence}) ${p.handle} — ${v.reason}`);
+      } catch (e) {
+        errs++; results.push({ ...p, verdict: 'ERROR', reason: e.message });
+      }
+      done++;
+    }));
+    process.stdout.write(`\r  …${done}/${rows.length} (errors ${errs})   `);
+  }
+  console.log('\n');
+
+  const flagged = results.filter(r => r.verdict === 'MULTI_ITEM' || r.verdict === 'ROOM_SPARSE');
+  const cost = (results.length * PRICE_PER_IMG);
+  writeFileSync(join(HERE, 'audit-results.json'), JSON.stringify({ scanned: results.length, flagged: flagged.length, cost_usd: cost.toFixed(3), results }, null, 2));
+  // CSV review list
+  const csv = ['handle,dw_sku,verdict,confidence,reason,image_url',
+    ...flagged.map(r => [r.handle, r.dw_sku || '', r.verdict, r.confidence || '', JSON.stringify(r.reason || ''), r.image_url].join(','))].join('\n');
+  writeFileSync(join(HERE, 'flagged.csv'), csv);
+  // HTML viewer
+  const html = `<!doctype html><meta charset=utf8><title>Innovations image flags</title>
+<style>body{font:14px system-ui;margin:20px}h1{font-size:18px}.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px}.c{border:1px solid #ddd;border-radius:8px;padding:8px}.c img{width:100%;height:180px;object-fit:cover;border-radius:4px}.v{font-weight:600}.MULTI_ITEM{color:#b45309}.ROOM_SPARSE{color:#7c3aed}</style>
+<h1>Innovations USA — flagged primary images (${flagged.length} of ${results.length}, $${cost.toFixed(3)})</h1>
+<div class=g>${flagged.map(r => `<div class=c><img loading=lazy src="${r.image_url}"><div class="v ${r.verdict}">${r.verdict} ${r.confidence ?? ''}</div><div>${r.reason || ''}</div><div style="font-size:12px;color:#666">${r.handle}<br>${r.dw_sku || ''}</div></div>`).join('')}</div>`;
+  writeFileSync(join(HERE, 'flagged.html'), html);
+
+  const byV = results.reduce((a, r) => (a[r.verdict] = (a[r.verdict] || 0) + 1, a), {});
+  console.log('Verdicts:', JSON.stringify(byV));
+  console.log(`FLAGGED: ${flagged.length} (${flagged.filter(r=>r.verdict==='MULTI_ITEM').length} multi-item, ${flagged.filter(r=>r.verdict==='ROOM_SPARSE').length} room-sparse)`);
+  console.log(`Cost: ~$${cost.toFixed(3)} (${results.length} imgs @ $${PRICE_PER_IMG})`);
+  console.log(`→ ${join(HERE, 'flagged.html')} | flagged.csv | audit-results.json`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/innovations-mfr-reconcile/archive-discontinued.mjs.pre-pg-decouple b/scripts/innovations-mfr-reconcile/archive-discontinued.mjs.pre-pg-decouple
new file mode 100644
index 0000000..1bec872
--- /dev/null
+++ b/scripts/innovations-mfr-reconcile/archive-discontinued.mjs.pre-pg-decouple
@@ -0,0 +1,83 @@
+#!/usr/bin/env node
+/**
+ * Archive Innovations products DISCONTINUED on the mfr site (READ-ONLY by default).
+ * Source: reconcile.json status==='DISCONTINUED' — each has a real SKU that is absent
+ * from the mfr pattern page AND returns 404 on its own item page (double-verified).
+ * Per DW rule: discontinued → status:'archived' (never draft, never delete; preserves SEO/orders).
+ *
+ * GATED: --execute requires SCRUB_CONFIRM=I-AM-STEVE. Default = dry-run.
+ * PG-first (mirror status) + direct productUpdate (status:ARCHIVED). Reversible: restore-map
+ * captures prev status; un-archive = set status back to ACTIVE.
+ */
+import { readFileSync, appendFileSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+import { gql } from '../lib/shopify.mjs';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const EXECUTE = process.argv.includes('--execute');
+const CONFIRMED = process.env.SCRUB_CONFIRM === 'I-AM-STEVE';
+
+const MUT = `mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id status } userErrors{ field message } } }`;
+const BASE = 'https://www.innovationsusa.com';
+const SLUG = { rangoon: 'rangoon-silk' };
+
+// Live re-verify at execute time (officer revision #1): a row is only archived if its own
+// mfr item page is GONE right now (404 or redirected away) — guards against a stale reconcile.json
+// archiving a SKU the mfr has re-listed. Returns true = confirmed discontinued.
+async function confirmGoneLive(row) {
+  const slug = SLUG[row.patt] || row.patt;
+  const sku = (row.mfr_sku || row.suffix).toLowerCase();
+  const url = `${BASE}/item/${slug}/${sku}`;
+  try {
+    const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' }, redirect: 'follow' });
+    // HARD gate (officer rev): archive ONLY on a genuine 404. A 5xx/timeout/redirect is NOT
+    // proof of discontinuation → treat as still-live (skip), never archive on ambiguity.
+    const gone = r.status === 404;
+    return { gone, url, code: r.status, finalUrl: r.url };
+  } catch (e) { return { gone: false, url, code: 'ERR', err: e.message }; }
+}
+
+async function main() {
+  const data = JSON.parse(readFileSync(join(HERE, 'reconcile.json'), 'utf8'));
+  const rows = data.classify.filter(r => r.status === 'DISCONTINUED');
+  console.log(`\n=== Archive ${rows.length} DISCONTINUED Innovations products — ${EXECUTE ? 'EXECUTE' : 'DRY-RUN'} ===\n`);
+  rows.forEach(r => console.log(`  ${r.suffix.padEnd(10)} ${r.dw_sku || '(no dw_sku)'}  ${r.handle}`));
+
+  if (!EXECUTE) { console.log('\nDRY-RUN: nothing changed. Re-run with --execute + SCRUB_CONFIRM=I-AM-STEVE.'); return; }
+  if (!CONFIRMED) { console.error('\n⛔ --execute refused: set SCRUB_CONFIRM=I-AM-STEVE (Steve only).'); process.exit(2); }
+
+  const require = createRequire(import.meta.url);
+  let pg; try { pg = require('pg'); } catch { pg = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/pg'); }
+  const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp' });
+  const log = join(process.env.HOME, '.claude/yolo-queue/restore-maps/innovations-archive-discontinued.jsonl');
+  let ok = 0, skipped = 0; const errs = [];
+  for (const r of rows) {
+    // Officer rev #1: live re-confirm the item page is gone before archiving.
+    const v = await confirmGoneLive(r);
+    if (!v.gone) { skipped++; console.log(`  ⏭ SKIP ${r.suffix} — still LIVE (${v.code} ${v.finalUrl || v.url}); not archiving`); continue; }
+    appendFileSync(log, JSON.stringify({ ts: new Date().toISOString(), handle: r.handle, shopify_id: r.shopify_id, prev_status: 'ACTIVE', live_check: v }) + '\n');
+    const res = await gql(MUT, { input: { id: r.shopify_id, status: 'ARCHIVED' } });
+    const ue = res?.productUpdate?.userErrors || [];
+    if (ue.length) { errs.push(`${r.handle}: ${JSON.stringify(ue)}`); continue; }
+    await pool.query(`UPDATE shopify_products SET status='archived' WHERE shopify_id=$1`, [r.shopify_id]);
+    ok++;
+    // GMC self-clean (Steve-approved 2026-07-10): delete this product's Google offer at the moment
+    // of a confirmed archive so an orphan never forms. Fail-open — a GMC error never breaks the
+    // archive. Disable by setting GMC_SELFCLEAN=0. The daily reconcile canary is the catch-all net.
+    if (process.env.GMC_SELFCLEAN !== '0') {
+      try {
+        const pid = String(r.shopify_id).match(/(\d+)\s*$/)?.[1];
+        if (pid) {
+          const { execFileSync } = await import('node:child_process');
+          execFileSync('node', [`${process.env.HOME}/.claude/skills/google-merchant-agent/gmc-delete-for-product.mjs`, `--product=${pid}`, '--apply', '--yes-i-am-steve'], { stdio: 'ignore', timeout: 60000 });
+        }
+      } catch { /* fail-open: never break the archive on a GMC cleanup error */ }
+    }
+  }
+  await pool.end();
+  console.log(`\n✅ Archived ${ok}/${rows.length} (skipped ${skipped} still-live), errors=${errs.length}. restore-map → ${log}`);
+  if (errs.length) console.log(errs.join('\n'));
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/innovations-mfr-reconcile/reconcile.mjs.pre-pg-decouple b/scripts/innovations-mfr-reconcile/reconcile.mjs.pre-pg-decouple
new file mode 100644
index 0000000..5e9e2e5
--- /dev/null
+++ b/scripts/innovations-mfr-reconcile/reconcile.mjs.pre-pg-decouple
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * Innovations USA mfr reconciliation (READ-ONLY).
+ * Fetches the LIVE mfr catalog (innovationsusa.com) for the patterns our active
+ * products belong to, builds the set of currently-live SKUs, then classifies each
+ * of our 221 active Innovations products:
+ *   - LIVE         : derived SKU is still on the mfr site
+ *   - DISCONTINUED : derived SKU is a real SKU but NO LONGER on the mfr → archive candidate
+ *   - NO_SKU_MATCH : handle suffix is an enumeration (sumatra-5, geode-12, innovations_zion),
+ *                    not a real SKU → can't auto-decide, needs manual
+ * Also captures, per live SKU, the clean 900x900 swatch image URL (for the image fix).
+ * Writes a proposal JSON+CSV. NO catalog write.
+ */
+import { writeFileSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/537.36';
+const BASE = 'https://www.innovationsusa.com';
+
+// our-parsed-pattern → mfr slug (only where they differ)
+const SLUG = { rangoon: 'rangoon-silk' };
+const SKIP_PATTERNS = new Set(['by']); // 'by-...' handles carry the real SKU in the suffix; covered by other pattern pages
+
+const isRealSku = (s) => /^[A-Z]{2,4}-?\d{2,4}$/.test(s); // ORI20, J602, RS8493, SUM-221, VAN-001, HUN-05
+
+async function fetchText(url) {
+  const r = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow' });
+  return r.ok ? await r.text() : '';
+}
+
+async function main() {
+  const require = createRequire(import.meta.url);
+  let pg; try { pg = require('pg'); } catch { pg = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/pg'); }
+  const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp' });
+  const { rows: prods } = await pool.query(
+    `SELECT handle, dw_sku, title, image_url, shopify_id,
+            split_part(regexp_replace(handle,'-by-innovations.*$',''),'-',1) AS patt,
+            upper(regexp_replace(handle,'^.*-dwc-','')) AS suffix
+     FROM shopify_products WHERE lower(status)='active' AND vendor ILIKE '%innovation%' ORDER BY handle`);
+  await pool.end();
+
+  const patterns = [...new Set(prods.map(p => p.patt))].filter(p => !SKIP_PATTERNS.has(p));
+  console.log(`Fetching ${patterns.length} mfr pattern pages…`);
+
+  const liveSkus = new Set();         // all live SKUs (uppercased, normalized no-dash)
+  const liveSkuRaw = new Map();       // norm → raw mfr sku (with dash)
+  const swatchBySku = new Map();      // norm → 900x900 swatch url
+  for (const p of patterns) {
+    const slug = SLUG[p] || p;
+    const html = await fetchText(`${BASE}/item/${slug}`) || await fetchText(`${BASE}/item/${slug}/`);
+    if (!html) { console.log(`  ⚠ ${slug}: no page`); continue; }
+    const skus = [...html.matchAll(new RegExp(`/item/${slug}/([a-z0-9-]+)`, 'gi'))].map(m => m[1].toUpperCase());
+    const uniq = [...new Set(skus)];
+    uniq.forEach(s => { const n = s.replace(/-/g, ''); liveSkus.add(n); liveSkuRaw.set(n, s); });
+    // swatch urls present on the page
+    [...html.matchAll(/storage\/sku\/900x900\/([A-Z0-9-]+)\.jpg/gi)].forEach(m => {
+      const n = m[1].toUpperCase().replace(/-/g, ''); swatchBySku.set(n, `${BASE}/storage/sku/900x900/${m[1]}.jpg`);
+    });
+    console.log(`  ${slug}: ${uniq.length} live SKUs`);
+  }
+  console.log(`Total live SKUs: ${liveSkus.size}\n`);
+
+  const classify = prods.map(p => {
+    const norm = p.suffix.replace(/-/g, '');
+    if (!isRealSku(p.suffix)) return { ...p, status: 'NO_SKU_MATCH', mfr_sku: null };
+    const live = liveSkus.has(norm);
+    return { ...p, status: live ? 'LIVE' : 'DISCONTINUED', mfr_sku: liveSkuRaw.get(norm) || p.suffix,
+             swatch: swatchBySku.get(norm) || (live ? `${BASE}/storage/sku/900x900/${liveSkuRaw.get(norm)}.jpg` : null) };
+  });
+
+  const byStatus = classify.reduce((a, r) => (a[r.status] = (a[r.status] || 0) + 1, a), {});
+  const discontinued = classify.filter(r => r.status === 'DISCONTINUED');
+  const noMatch = classify.filter(r => r.status === 'NO_SKU_MATCH');
+  console.log('CLASSIFICATION:', JSON.stringify(byStatus));
+  console.log(`\nDISCONTINUED (archive candidates): ${discontinued.length}`);
+  discontinued.forEach(r => console.log(`  ${r.suffix.padEnd(10)} ${r.dw_sku || '(no dw_sku)'}  ${r.handle}`));
+  console.log(`\nNO_SKU_MATCH (enumerated handles, manual): ${noMatch.length} — e.g. ${noMatch.slice(0,8).map(r=>r.suffix).join(', ')}`);
+
+  writeFileSync(join(HERE, 'reconcile.json'), JSON.stringify({ liveSkuCount: liveSkus.size, byStatus, classify }, null, 2));
+  const csv = ['handle,dw_sku,suffix,status,mfr_sku,swatch,current_image',
+    ...classify.map(r => [r.handle, r.dw_sku||'', r.suffix, r.status, r.mfr_sku||'', r.swatch||'', r.image_url].join(','))].join('\n');
+  writeFileSync(join(HERE, 'reconcile.csv'), csv);
+  console.log(`\n→ ${join(HERE,'reconcile.json')} | reconcile.csv`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/lib/inventory-stamp-guard.mjs.pre-shared-lift b/scripts/lib/inventory-stamp-guard.mjs.pre-shared-lift
new file mode 100644
index 0000000..29d1810
--- /dev/null
+++ b/scripts/lib/inventory-stamp-guard.mjs.pre-shared-lift
@@ -0,0 +1,80 @@
+// VENDORED COPY — canonical source: Designer-Wallcoverings/shopify/scripts/lib/inventory-stamp-guard.mjs
+// Vendored (not cross-repo-imported) on purpose: this repo ships and runs independently, and a
+// cross-repo relative import would hard-crash the scheduled cadence if either tree moved.
+// KEEP IN SYNC — the TK-11357 fixture harness hashes both copies and FAILS on drift.
+// TK-11357 (lineage TK-10825/10965/11140/11299/11301/11357).
+// TK-10965 — Fix B (prevention): the inventory-stamp invariant, as a pure guard.
+//
+// ROOT CAUSE (see ../FINDINGS.md): importers stamp a positive "cap-free" stock
+// number (the year literal 2026) on the SELLABLE non-Sample variant of every
+// activated product — both in the product-create payload (`inventory_quantity: 2026`)
+// and on reconcile (`setInventory2026()`). When that sellable variant is ALSO
+// priced $0 (quote-only / contact-for-price lines like Phillipe Romano, Fentucci
+// Naturals), positive stock makes it `availableForSale` → checkout-orderable for $0.
+//
+// THE INVARIANT this module enforces (one place, both call sites):
+//   A sellable variant that is priced $0 OR belongs to a quote-only / price-
+//   suppressed line must NEVER receive positive inventory. It gets 0 → not orderable.
+//   (The $4.25 Sample variant is unaffected — it is not the sellable variant and is
+//    already qty=0/non-orderable by design.)
+//
+// PURE + dependency-free on purpose: no network, no env, no Shopify client, so it
+// unit-tests offline and drops into any importer runtime unchanged. $0 (local).
+
+// Tag family that means "this line has no public retail price" — a superset of the
+// single `quote-only` tag the standing canary keyed on (which is why Fentucci, tagged
+// `quotes`/`Needs-Price`, was the canary's 462-product blind spot).
+export const PRICE_SUPPRESSED_TAGS = new Set([
+  'quote-only', 'quote only', 'quote_only',
+  'quotes', 'contact-for-price', 'contact for price', 'needs-price', 'needs price',
+]);
+
+const norm = t => String(t).trim().toLowerCase();
+
+/**
+ * Is this product a quote-only / price-suppressed line?
+ * @param {{tags?: string[]|string, vendor?: string}} product
+ */
+export function isPriceSuppressed(product = {}) {
+  const tags = Array.isArray(product.tags)
+    ? product.tags
+    : String(product.tags || '').split(',');
+  if (tags.some(t => PRICE_SUPPRESSED_TAGS.has(norm(t)))) return true;
+  // Vendor fallback for untagged cohorts (Fentucci Naturals ships quote-only with
+  // zero quote-only tags). Extend as new price-on-request lines are onboarded.
+  return norm(product.vendor) === 'fentucci naturals';
+}
+
+/**
+ * A variant is the "sellable" one iff it is NOT the Sample variant.
+ * (Importers create exactly two variants: `Sample` @ $4.25 and the real unit @ price.)
+ * @param {{title?: string, option1?: string}} variant
+ */
+export function isSellableVariant(variant = {}) {
+  const label = variant.title ?? variant.option1 ?? '';
+  return !/sample/i.test(label);
+}
+
+/**
+ * Would giving this sellable variant positive stock make it a $0-orderable defect?
+ * True iff it's the sellable variant AND (price is 0 OR the line is price-suppressed).
+ * @param {object} variant  the variant about to be stamped
+ * @param {object} product  its parent (for tags/vendor)
+ */
+export function isZeroPriceOrderableRisk(variant = {}, product = {}) {
+  if (!isSellableVariant(variant)) return false;
+  const price = Number(variant.price);
+  return price === 0 || Number.isNaN(price) || isPriceSuppressed(product);
+}
+
+/**
+ * THE GUARD. Return the inventory quantity that is SAFE to stamp on this variant.
+ * Drop-in replacement for the literal `2026` at both call sites:
+ *   - create payload:  inventory_quantity: safeStampQuantity(variant, product)
+ *   - setInventory2026: quantity:        safeStampQuantity(variant, product)
+ * Returns `desired` (2026) for normal priced variants; 0 for the defect class.
+ * @returns {number} 0 for a zero-price-orderable risk, else `desired`
+ */
+export function safeStampQuantity(variant, product, desired = 2026) {
+  return isZeroPriceOrderableRisk(variant, product) ? 0 : desired;
+}
diff --git a/scripts/title-campaign/build-titles.mjs.pre-pg-decouple b/scripts/title-campaign/build-titles.mjs.pre-pg-decouple
new file mode 100644
index 0000000..6244277
--- /dev/null
+++ b/scripts/title-campaign/build-titles.mjs.pre-pg-decouple
@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+/**
+ * 18,599 NON-UNIQUE-TITLE rewrite campaign driver  (DRY-RUN by default).
+ *
+ * Mirrors the safety pattern of scripts/innovations-mfr-reconcile/archive-discontinued.mjs:
+ *   - DRY-RUN unless BOTH `--execute` AND env SCRUB_CONFIRM=I-AM-STEVE are present.
+ *   - PostgreSQL mirror (dw_unified) is the SOURCE OF TRUTH; reads happen there.
+ *   - On --execute: restore-map (old title per id) is appended FIRST, then the {id,title}
+ *     PUT is sent (idempotent — re-running the same row is a no-op once the title matches).
+ *
+ * DTD-committed formula (Option B):  NEW title = `<pattern_name> <colorway> | <vendor>`
+ *   colorway precedence (per row):
+ *     1. structured `color:<Name>` tag  IF the value is a sane color word
+ *        (interior-design colorway lexicon) — NOT a status flag like `AI-Analyzed-v2`.
+ *     2. else AI `dominant_color` from shopify_color_enrichment (joined on handle).
+ *     3. else (only on the RESIDUAL that STILL collides after pattern+colorway) a
+ *        `dw_sku`-derived suffix to break the tie.
+ *   HOLD any row with no pattern_name (do NOT fabricate one).
+ *
+ * HARD GATES:
+ *   (1) UNIQUENESS-GATED — only rows whose CURRENT title is a collision-group member
+ *       (exact title shared across >1 distinct handle, within ACTIVE) are eligible.
+ *       Already-unique titles are NEVER touched.
+ *   (2) GSC-keyword-preserving — pattern_name + vendor tokens are always kept in the new title.
+ *   (3) Banned word "Wallpaper" folded out IN THE SAME WRITE, using the brand-suffix
+ *       guard from bannedword-title-preview: only the descriptor (text before ' | ') is
+ *       cleaned; a legitimate brand suffix containing the word is left intact — EXCEPT the
+ *       known DW canonical-vendor normalization `| Versace Wallcovering` -> `| Versace`
+ *       (vendor string in the mirror is `Versace`, the suffix variant is legacy).
+ *
+ * USAGE:
+ *   node build-titles.mjs --vendor "Versace"              # DRY-RUN, writes QA + worklist JSON
+ *   node build-titles.mjs --vendor "Versace" --qa 20      # set QA sample size
+ *   SCRUB_CONFIRM=I-AM-STEVE node build-titles.mjs --vendor "Versace" --execute   # GATED write
+ *
+ * "max it" applies to the LOCAL colorway-resolution COMPUTE (this whole resolve pass is
+ * already a single in-memory vectorized pass over the vendor set — fanning out across the
+ * 18,599 is free locally). It does NOT apply to the Shopify writes: those go direct Admin
+ * API at ~2/sec; saturating the API only THROTTLES (the gql() helper backs off on THROTTLED).
+ */
+import { readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+import { execFileSync } from 'node:child_process';
+import { gql } from '../lib/shopify.mjs';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const OUT = join(HERE, 'out');
+mkdirSync(OUT, { recursive: true });
+
+const argv = process.argv.slice(2);
+const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : d; };
+const VENDOR = arg('--vendor', 'Versace');
+const QA_N = parseInt(arg('--qa', '20'), 10);
+const EXECUTE = argv.includes('--execute');
+const CONFIRMED = process.env.SCRUB_CONFIRM === 'I-AM-STEVE';
+const SLUG = VENDOR.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+
+const PSQL = ['/opt/homebrew/opt/postgresql@14/bin/psql', '/usr/local/opt/postgresql@14/bin/psql', 'psql']
+  .find(p => { try { execFileSync(p, ['--version'], { stdio: 'ignore' }); return true; } catch { return false; } }) || 'psql';
+const DB = process.env.DATABASE_URL || process.env.DW_UNIFIED_URL || 'postgresql:///dw_unified?host=/tmp';
+function q(sql) {
+  const out = execFileSync(PSQL, [DB, '-At', '-F', '\t', '-c', sql], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
+  return out.trim() ? out.trim().split('\n').map(r => r.split('\t')) : [];
+}
+const esc = s => String(s).replace(/'/g, "''");
+
+// ---- sane-colorway lexicon (interior-designer colorway vocabulary) -----------------
+// A `color:<X>` tag value is only trusted if it matches a real color WORD set. Anything
+// outside this set (e.g. a leaked status flag like `AI-Analyzed-v2`) is rejected and we
+// fall through to the AI dominant_color. The set is generous: base hues + the designer
+// colorway names that show up in the DW catalog (Oatmeal/Alabaster/Greige/Celadon...).
+const COLOR_WORDS = new Set([
+  // base hues / neutrals
+  'white','black','gray','grey','red','blue','green','yellow','orange','purple','pink','brown',
+  'beige','tan','cream','ivory','gold','silver','bronze','brass','copper','navy','teal','aqua',
+  'turquoise','maroon','burgundy','olive','lime','indigo','violet','magenta','coral','salmon',
+  'peach','mint','lavender','plum','rust','mustard','charcoal','slate','taupe','khaki','sand',
+  'stone','clay','rose','ruby','emerald','sapphire','amber','jade','pearl','platinum','chrome',
+  // designer colorway names
+  'oatmeal','alabaster','greige','celadon','ecru','bone','eggshell','linen','chalk','snow',
+  'oyster','mushroom','fawn','camel','caramel','coffee','espresso','chocolate','mocha','cocoa',
+  'honey','wheat','straw','flax','putty','dove','ash','smoke','pewter','graphite','gunmetal',
+  'jet','ebony','onyx','midnight','ink','navy blue','denim','indigo','cobalt','peacock','cerulean',
+  'sage','fern','moss','forest','hunter','pine','spruce','basil','pistachio','seafoam','celery',
+  'ochre','ocher','saffron','turmeric','marigold','goldenrod','apricot','terracotta','sienna',
+  'paprika','brick','cinnamon','mahogany','chestnut','walnut','hazel','sepia','umber','wine',
+  'merlot','claret','blush','mauve','orchid','lilac','heather','periwinkle','wisteria',
+  'off white','off-white','antique white','warm white','soft white','natural','flax','nude',
+  'champagne','vanilla','buttercream','parchment','sandstone','driftwood','greystone','scarlet',
+  'crimson','vermillion','vermilion','cardinal','fuchsia','aubergine','eggplant','grape','currant',
+]);
+function isSaneColor(v) {
+  if (!v) return false;
+  const t = v.trim().toLowerCase();
+  if (!t || t.length > 24) return false;
+  if (/v\d|analyz|status|needs-|tag$|^ai\b/i.test(t)) return false; // obvious status-flag shapes
+  // accept if every word of a multi-word value (e.g. "Hunter Green") is a known color word,
+  // or the whole phrase is a known colorway name.
+  if (COLOR_WORDS.has(t)) return true;
+  const parts = t.split(/\s+/);
+  return parts.length > 1 && parts.every(p => COLOR_WORDS.has(p));
+}
+const TC = s => String(s).replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
+
+// ---- banned-word "Wallpaper" guard (descriptor-only) -------------------------------
+const WP = /\bwallpaper\b/gi;
+function bannedClean(title) {
+  // canonical-vendor normalization first (legacy suffix -> mirror vendor string)
+  let t = title.replace(/\|\s*Versace Wallcovering\b/gi, '| Versace');
+  const parts = t.split(' | ');
+  if (parts.length > 1) {
+    parts[0] = parts[0].replace(WP, m => (m[0] === 'W' ? 'Wallcovering' : 'wallcovering'));
+    return parts.join(' | ');
+  }
+  return t.replace(WP, m => (m[0] === 'W' ? 'Wallcovering' : 'wallcovering'));
+}
+
+// ---- LOAD: collision-group members for this vendor ----------------------------------
+console.log(`\n=== Title-campaign · vendor="${VENDOR}" · ${EXECUTE ? 'EXECUTE' : 'DRY-RUN'} ===\n`);
+const rows = q(`
+  with grp as (
+    select trim(title) ti from shopify_products
+    where status='ACTIVE' and vendor ilike '%${esc(VENDOR)}%' and trim(coalesce(title,''))<>''
+    group by trim(title) having count(distinct handle) > 1
+  )
+  select p.shopify_id, p.handle, p.title, p.pattern_name, p.dw_sku, p.mfr_sku,
+         nullif(trim((regexp_match(p.tags,'color:([^,]+)'))[1]),'') as color_tag,
+         nullif(trim(e.dominant_color),'') as dom_color
+  from shopify_products p
+  left join shopify_color_enrichment e on e.handle = p.handle
+  where p.status='ACTIVE' and p.vendor ilike '%${esc(VENDOR)}%' and trim(p.title) in (select ti from grp)
+  order by p.title, p.handle
+`).map(r => ({
+  shopify_id: r[0], handle: r[1], old_title: r[2], pattern_name: r[3],
+  dw_sku: r[4], mfr_sku: r[5], color_tag: r[6] || null, dom_color: r[7] || null,
+}));
+
+if (!rows.length) { console.log(`No collision-group members found for "${VENDOR}". Nothing to do.`); process.exit(0); }
+
+// ---- PASS 1: resolve pattern + colorway + source (LOCAL compute, single vectorized pass) ----
+for (const r of rows) {
+  const pat = (r.pattern_name || '').trim();
+  if (!pat) { r.hold = 'no-pattern-name'; continue; }
+  let colorway = null, src = null;
+  if (isSaneColor(r.color_tag)) { colorway = TC(r.color_tag.trim()); src = 'color-tag'; }
+  else if (r.dom_color && r.dom_color.trim()) { colorway = TC(r.dom_color.trim()); src = 'ai-dominant'; }
+  r.pat = pat; r.colorway = colorway; r.colorway_src = src;
+  // provisional new title BEFORE tiebreak
+  const body = colorway ? `${TC(pat)} ${colorway}` : TC(pat);
+  r.base_title = bannedClean(`${body} | ${VENDOR}`);
+}
+
+// ---- PASS 2: find residual collisions (same base_title across >1 handle) → dw_sku tiebreak ----
+const byBase = {};
+for (const r of rows) if (!r.hold) (byBase[r.base_title] ||= []).push(r);
+let tiebreaks = 0;
+for (const [, grp] of Object.entries(byBase)) {
+  const distinctHandles = new Set(grp.map(g => g.handle));
+  if (distinctHandles.size <= 1) { for (const g of grp) { g.new_title = g.base_title; g.colorway_src = g.colorway_src || 'pattern-only'; } continue; }
+  // residual collision — append a dw_sku-derived suffix to break the tie.
+  // Guard the suffix SOURCE: a `...SAMPLE` dw_sku is a sample-variant artifact, never a
+  // customer-facing differentiator; a `copy-of-*` handle is a junk duplicate row. In both
+  // cases HOLD rather than emit an ugly/contaminated title (conservative + reversible).
+  for (const g of grp) {
+    if (!g.dw_sku || !g.dw_sku.trim()) { g.hold = 'residual-collision-no-dwsku'; continue; }
+    if (/sample/i.test(g.dw_sku)) { g.hold = 'residual-collision-sample-sku'; continue; }
+    if (/^copy-of-/i.test(g.handle)) { g.hold = 'residual-collision-copy-of-junk'; continue; }
+    const suffix = g.dw_sku.trim();
+    const parts = g.base_title.split(' | ');
+    g.new_title = parts.length > 1 ? `${parts[0]} ${suffix} | ${parts.slice(1).join(' | ')}` : `${g.base_title} ${suffix}`;
+    g.colorway_src = (g.colorway_src || 'pattern-only') + '+dwsku-tiebreak';
+    tiebreaks++;
+  }
+}
+
+// ---- guard: never EMIT a write that re-collides or that no-ops (already-correct) -------
+const newTitleCount = {};
+for (const r of rows) if (!r.hold && r.new_title) newTitleCount[r.new_title] = (newTitleCount[r.new_title] || 0) + 1;
+const writes = [], holds = [], noops = [];
+for (const r of rows) {
+  if (r.hold) { holds.push(r); continue; }
+  if (!r.new_title) { r.hold = 'unresolved'; holds.push(r); continue; }
+  if (newTitleCount[r.new_title] > 1) { r.hold = 'would-still-collide'; holds.push(r); continue; }
+  if (r.new_title === r.old_title) { noops.push(r); continue; } // idempotent: already correct
+  writes.push(r);
+}
+
+// ---- stats ----
+const stats = {
+  vendor: VENDOR, generated_at: new Date().toISOString(),
+  collision_members: rows.length,
+  used_color_tag: rows.filter(r => r.colorway_src && r.colorway_src.startsWith('color-tag')).length,
+  used_ai_dominant: rows.filter(r => r.colorway_src && r.colorway_src.startsWith('ai-dominant')).length,
+  used_pattern_only: rows.filter(r => r.colorway_src && r.colorway_src.startsWith('pattern-only')).length,
+  needing_dwsku_tiebreak: tiebreaks,
+  held: holds.length,
+  held_reasons: holds.reduce((a, r) => (a[r.hold] = (a[r.hold] || 0) + 1, a), {}),
+  noops_already_correct: noops.length,
+  writes_pending: writes.length,
+};
+
+// ---- QA sample (across distinct collision groups for variety) ----
+const seenGroup = new Set(); const qa = [];
+for (const r of [...writes, ...holds]) {
+  if (qa.length >= QA_N) break;
+  const gk = r.old_title; if (seenGroup.has(gk) && qa.length > QA_N / 2) continue; seenGroup.add(gk);
+  qa.push({
+    handle: r.handle, old_title: r.old_title,
+    new_title: r.hold ? `(HOLD: ${r.hold})` : r.new_title,
+    colorway_source: r.hold ? '-' : r.colorway_src,
+  });
+}
+
+// ---- write artifacts ----
+const worklist = writes.map(r => ({ shopify_id: r.shopify_id, handle: r.handle, dw_sku: r.dw_sku, old_title: r.old_title, new_title: r.new_title, colorway_src: r.colorway_src }));
+writeFileSync(join(OUT, `${SLUG}-worklist.json`), JSON.stringify({ stats, worklist }, null, 2));
+writeFileSync(join(OUT, `${SLUG}-holds.json`), JSON.stringify(holds.map(r => ({ handle: r.handle, old_title: r.old_title, reason: r.hold, pattern_name: r.pattern_name, dw_sku: r.dw_sku })), null, 2));
+
+console.log('STATS', JSON.stringify(stats, null, 2));
+console.log(`\n--- QA sample (${qa.length} rows) ---`);
+console.log('handle | OLD title | NEW title | colorway-src');
+for (const x of qa) console.log(`${x.handle} | ${x.old_title} | ${x.new_title} | ${x.colorway_source}`);
+console.log(`\nartifacts: ${join(OUT, SLUG + '-worklist.json')}  +  ${join(OUT, SLUG + '-holds.json')}`);
+
+if (!EXECUTE) { console.log(`\nDRY-RUN: nothing written to Shopify. Re-run with --execute + SCRUB_CONFIRM=I-AM-STEVE after QA approval.`); process.exit(0); }
+if (!CONFIRMED) { console.error(`\n⛔ --execute refused: set SCRUB_CONFIRM=I-AM-STEVE (Steve only).`); process.exit(2); }
+
+// ===== GATED WRITE PATH (only reached on --execute + SCRUB_CONFIRM=I-AM-STEVE) ==========
+const require = createRequire(import.meta.url);
+let pg; try { pg = require('pg'); } catch { pg = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/pg'); }
+const pool = new pg.Pool({ connectionString: DB });
+const restoreLog = join(process.env.HOME, `.claude/yolo-queue/restore-maps/title-campaign-${SLUG}-${new Date().toISOString().slice(0, 10)}.jsonl`);
+const MUT = `mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id title } userErrors{ field message } } }`;
+let ok = 0; const errs = [];
+for (const r of writes) {
+  // restore-map FIRST (reversible: old title per id) — written before the PUT
+  appendFileSync(restoreLog, JSON.stringify({ ts: new Date().toISOString(), shopify_id: r.shopify_id, handle: r.handle, old_title: r.old_title, new_title: r.new_title }) + '\n');
+  const res = await gql(MUT, { input: { id: r.shopify_id, title: r.new_title } });
+  const ue = res?.productUpdate?.userErrors || res?.__err || [];
+  if (ue.length) { errs.push(`${r.handle}: ${JSON.stringify(ue)}`); continue; }
+  await pool.query(`UPDATE shopify_products SET title=$1 WHERE shopify_id=$2`, [r.new_title, r.shopify_id]);
+  ok++;
+}
+await pool.end();
+console.log(`\n✅ Rewrote ${ok}/${writes.length}, errors=${errs.length}. restore-map → ${restoreLog}`);
+if (errs.length) console.log(errs.join('\n'));
diff --git a/scripts/true-sku-leak-scrub/scrub-true-sku.mjs.pre-pg-decouple b/scripts/true-sku-leak-scrub/scrub-true-sku.mjs.pre-pg-decouple
new file mode 100644
index 0000000..d7cbf5d
--- /dev/null
+++ b/scripts/true-sku-leak-scrub/scrub-true-sku.mjs.pre-pg-decouple
@@ -0,0 +1,239 @@
+#!/usr/bin/env node
+/**
+ * true-sku-leak scrub driver  (Part 2 of true-sku-leak-fix-2026-06-17)
+ * ------------------------------------------------------------------------
+ * Removes the literal boolean string "TRUE" that leaked into the MFR-SKU /
+ * barcode fields of 91 ACTIVE products (root cause: a Variant-Barcode CSV
+ * column shift; Part 1 source gate already committed so it cannot re-leak).
+ *
+ * SURGICAL + SCOPED — this is the decisive safety property:
+ *   • DELETE only metafields whose key is `manufacturer_sku` (any namespace:
+ *     custom / global / dwc) OR is global.`Variant Barcode`, AND whose value
+ *     is literally "TRUE".
+ *   • CLEAR the variant `barcode` field only where it is literally "TRUE".
+ *   • PRESERVE every other TRUE-valued metafield — Fentucci et al. carry
+ *     LEGITIMATE booleans (global.Published, Variant Taxable, Requires
+ *     Shipping, Included / United States, custom.showroom_line, …). A naive
+ *     "delete all == TRUE" would corrupt those. We never touch them.
+ *   • PRESERVE clean manufacturer_sku survivors (e.g. dwc.manufacturer_sku=77119).
+ *
+ * Status is NEVER changed — ACTIVE products stay ACTIVE.
+ *
+ * PG-FIRST + QUEUE (never direct-mutate Shopify variants):
+ *   1. UPDATE shopify_products SET mfr_sku=NULL where it is 'TRUE' (mirror).
+ *   2. Enqueue the Shopify writes into shopify_api_queue; the existing queue
+ *      runner executes them rate-limited. We do NOT call Shopify mutations here.
+ *   3. Append a per-row restore-map BEFORE writing → one-command reversible.
+ *
+ * GATING (yolo-plus): --execute performs gated writes (dw_unified + queued
+ * customer-facing Shopify). It refuses unless SCRUB_CONFIRM=I-AM-STEVE is set,
+ * so it cannot run autonomously. Default is DRY-RUN: resolves live (read-only),
+ * prints every row, writes the staging file, writes nothing else.
+ *
+ * Usage:
+ *   node scrub-true-sku.mjs            # DRY-RUN, canary (7 non-Innovations singletons)
+ *   node scrub-true-sku.mjs --all      # DRY-RUN, full 91
+ *   SCRUB_CONFIRM=I-AM-STEVE node scrub-true-sku.mjs --execute   # gated: PG + enqueue (canary)
+ */
+import { gql } from '../lib/shopify.mjs';
+import { readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const EXECUTE = process.argv.includes('--execute');
+const ALL = process.argv.includes('--all');
+const CONFIRMED = process.env.SCRUB_CONFIRM === 'I-AM-STEVE';
+const SOURCE_AGENT = 'true-sku-leak-scrub';
+const HOME = process.env.HOME;
+
+// The 7 non-Innovations singletons = canary. They sidestep ALL three officer-
+// required revisions (suffix-parse verify, vandal dw_sku dup, sumatra-9) which
+// are Innovations-only. Each resolves to NULL mfr_sku + cleared TRUE barcode.
+const CANARY_HANDLES = new Set([
+  'dwh-77119', 'dwdg-986161', 'dwsa-303001-1', 'dwh-70515',
+  'dwpa-757834-designer-wallcoverings-los-angeles', 'dwve-430000', 'dwh-70320',
+]);
+
+// Officer revision #3: NEEDS-MANUAL row (no dw_sku) — never auto-process in a batch.
+const EXCLUDE_HANDLES = new Set([
+  'sumatra-by-innovations-usa-dwc-sumatra-9',
+]);
+
+// SCOPED leak predicate — the heart of the safety guarantee.
+const isLeakMetafield = (m) =>
+  String(m.value).toUpperCase() === 'TRUE' &&
+  (m.key === 'manufacturer_sku' || (m.namespace === 'global' && m.key === 'Variant Barcode'));
+
+const MUT_DELETE = `mutation($metafields:[MetafieldIdentifierInput!]!){
+  metafieldsDelete(metafields:$metafields){ deletedMetafields{ key namespace } userErrors{ field message } } }`;
+const MUT_BARCODE = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
+  productVariantsBulkUpdate(productId:$pid,variants:$variants){ productVariants{ id barcode } userErrors{ field message } } }`;
+const Q_RESOLVE = `query($id:ID!){ product(id:$id){ status
+  metafields(first:80){ edges{ node{ id namespace key value } } }
+  variants(first:20){ edges{ node{ id sku barcode } } } } }`;
+
+function loadWorklist() {
+  const tsv = readFileSync(join(HOME, '.claude/yolo-queue/true-sku-leak-worklist-2026-06-16.tsv'), 'utf8')
+    .trim().split('\n');
+  const hdr = tsv[0].split('\t');
+  return tsv.slice(1).filter(l => l && !l.startsWith('(')).map(l => {
+    const c = l.split('\t'); const o = {}; hdr.forEach((h, i) => o[h] = c[i]); return o;
+  });
+}
+function loadGidMap() {
+  // restore-map carries the GIDs (shopify_id, variant_id) keyed by handle
+  const csv = readFileSync(join(HOME, '.claude/yolo-queue/restore-maps/true-sku-leak-restore-20260618.csv'), 'utf8')
+    .trim().split('\n');
+  const hdr = csv[0].split(',');
+  const map = {};
+  csv.slice(1).forEach(l => {
+    // metafields col is JSON with commas → only need first 5 cols, split safely
+    const c = l.split(',');
+    const r = {}; hdr.forEach((h, i) => r[h] = c[i]);
+    map[r.handle] = { product_gid: r.shopify_id, variant_gid: r.variant_id, vendor: r.vendor };
+  });
+  return map;
+}
+
+async function buildPlan(rows, gidMap) {
+  const plan = [];
+  for (const row of rows) {
+    const g = gidMap[row.handle];
+    if (!g || !g.product_gid) { plan.push({ handle: row.handle, error: 'no GID in restore-map' }); continue; }
+    const p = (await gql(Q_RESOLVE, { id: g.product_gid }))?.product;
+    if (!p) { plan.push({ handle: row.handle, error: 'product not found live' }); continue; }
+    const delMfs = p.metafields.edges.map(e => e.node).filter(isLeakMetafield);
+    const preserved = p.metafields.edges.map(e => e.node)
+      .filter(m => String(m.value).toUpperCase() === 'TRUE' && !isLeakMetafield(m))
+      .map(m => `${m.namespace}.${m.key}`);
+    const trueVars = p.variants.edges.map(e => e.node)
+      .filter(v => v.barcode && String(v.barcode).toUpperCase() === 'TRUE');
+    plan.push({
+      handle: row.handle, vendor: g.vendor, product_gid: g.product_gid, status: p.status,
+      delete_metafields: delMfs.map(m => ({ namespace: m.namespace, key: m.key })),
+      clear_barcode_variants: trueVars.map(v => ({ id: v.id, sku: v.sku, prev_barcode: v.barcode })),
+      preserved_true_booleans: preserved,
+    });
+  }
+  return plan;
+}
+
+function queueRowsFor(item) {
+  const rows = [];
+  if (item.delete_metafields.length) {
+    rows.push({
+      method: 'POST', endpoint: 'graphql', token_alias: 'product', priority: 5,
+      status: 'pending', source_agent: SOURCE_AGENT,
+      payload: { query: MUT_DELETE, variables: {
+        metafields: item.delete_metafields.map(m => ({ ownerId: item.product_gid, namespace: m.namespace, key: m.key })) } },
+    });
+  }
+  if (item.clear_barcode_variants.length) {
+    rows.push({
+      method: 'POST', endpoint: 'graphql', token_alias: 'product', priority: 5,
+      status: 'pending', source_agent: SOURCE_AGENT,
+      payload: { query: MUT_BARCODE, variables: {
+        pid: item.product_gid,
+        variants: item.clear_barcode_variants.map(v => ({ id: v.id, barcode: '' })) } },
+    });
+  }
+  return rows;
+}
+
+async function main() {
+  const worklist = loadWorklist();
+  const gidMap = loadGidMap();
+  const rows = (ALL ? worklist : worklist.filter(r => CANARY_HANDLES.has(r.handle)))
+    .filter(r => !EXCLUDE_HANDLES.has(r.handle));
+  console.log(`\n=== true-sku-leak scrub — ${ALL ? 'FULL 91' : 'CANARY (7 singletons)'} — ${EXECUTE ? 'EXECUTE' : 'DRY-RUN'} ===\n`);
+
+  const plan = await buildPlan(rows, gidMap);
+  let totDel = 0, totBc = 0;
+  for (const it of plan) {
+    if (it.error) { console.log(`✗ ${it.handle}: ${it.error}`); continue; }
+    totDel += it.delete_metafields.length; totBc += it.clear_barcode_variants.length;
+    console.log(`— ${it.handle} (${it.vendor}) status=${it.status}`);
+    console.log(`   DELETE: ${it.delete_metafields.map(m => m.namespace + '.' + m.key).join(', ') || '(none)'}`);
+    console.log(`   CLEAR barcode: ${it.clear_barcode_variants.map(v => v.sku).join(', ') || '(none)'}`);
+    console.log(`   PRESERVE TRUE booleans: ${it.preserved_true_booleans.join(', ') || '(none)'}`);
+  }
+  console.log(`\nTOTAL: ${totDel} metafield deletes + ${totBc} barcode clears across ${plan.filter(p=>!p.error).length} products`);
+
+  // Always write the staging file (the exact queue rows that WOULD be inserted)
+  const staging = plan.filter(p => !p.error).map(it => ({ handle: it.handle, product_gid: it.product_gid, queue_rows: queueRowsFor(it) }));
+  const stagingPath = join(HERE, ALL ? 'queue-rows-full91.json' : 'canary-queue-rows.json');
+  writeFileSync(stagingPath, JSON.stringify({ note: 'EXACT shopify_api_queue rows. NOT inserted unless --execute + SCRUB_CONFIRM.', generated_for: ALL ? 'full-91' : 'canary-7', staging }, null, 2));
+  console.log(`staged → ${stagingPath}`);
+
+  if (!EXECUTE) {
+    console.log('\nDRY-RUN: no PG write, no queue insert. Re-run with --execute + SCRUB_CONFIRM=I-AM-STEVE to apply.');
+    return;
+  }
+  if (!CONFIRMED) {
+    console.error('\n⛔ --execute refused: gated write. Set SCRUB_CONFIRM=I-AM-STEVE to confirm (Steve only).');
+    process.exit(2);
+  }
+
+  // ---- GATED EXECUTE: PG-first mirror update + restore-map + (direct apply | enqueue) ----
+  // TRANSPORT NOTE: the local Mac2 dw_unified.shopify_api_queue has NO active runner
+  // (zero 'done' rows ever; a runner only exists Kamatera-side against the canonical DB).
+  // So --direct applies the approved EFFECT via gql() (idempotent: delete-already-gone /
+  // clear-already-empty are no-ops; reversible via the restore-map). Each batch is still
+  // Steve-greenlit. Without --direct it ENQUEUEs (kept for when the Kamatera path is wired).
+  const DIRECT = process.argv.includes('--direct');
+  // pg resolved lazily (house scripts run with it on the path); dry-run never needs it.
+  const require = createRequire(import.meta.url);
+  let pg;
+  try { pg = require('pg'); }
+  catch { pg = require('/Users/macstudio3/Projects/Designer-Wallcoverings/node_modules/pg'); }
+  const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp' });
+  const restoreDir = join(HOME, '.claude/yolo-queue/restore-maps');
+  mkdirSync(restoreDir, { recursive: true });
+  const execLog = join(restoreDir, `true-sku-leak-${DIRECT ? 'APPLY' : 'EXEC'}-${ALL ? 'full91' : 'canary'}.jsonl`);
+  let enq = 0, delMf = 0, clrBc = 0; const errs = [];
+  for (const it of plan) {
+    if (it.error) continue;
+    // restore-map row BEFORE any write
+    appendFileSync(execLog, JSON.stringify({ ts: new Date().toISOString(), ...it }) + '\n');
+    // 1. PG-first: clear the mirror's bogus TRUE (NULL it). Match the FULL GID — the
+    //    shopify_products.shopify_id column stores 'gid://shopify/Product/<id>', not the numeric id.
+    await pool.query(
+      `UPDATE shopify_products SET mfr_sku=NULL WHERE shopify_id=$1 AND upper(mfr_sku)='TRUE'`,
+      [it.product_gid],
+    );
+    // 2. Apply the Shopify writes
+    for (const qr of queueRowsFor(it)) {
+      if (DIRECT) {
+        const r = await gql(qr.payload.query, qr.payload.variables);
+        const md = r?.metafieldsDelete, bc = r?.productVariantsBulkUpdate;
+        const ue = (md?.userErrors || []).concat(bc?.userErrors || []);
+        if (ue.length) errs.push(`${it.handle}: ${JSON.stringify(ue)}`);
+        else if (md) delMf += (md.deletedMetafields || []).length;
+        else if (bc) clrBc += 1;
+      } else {
+        // request_hash is a GENERATED STORED column with NO unique constraint, so
+        // ON CONFLICT (request_hash) is invalid — guard with NOT EXISTS instead.
+        const r = await pool.query(
+          `INSERT INTO shopify_api_queue (method, endpoint, payload, token_alias, priority, status, source_agent)
+           SELECT $1,$2,$3,$4,$5,$6,$7
+           WHERE NOT EXISTS (SELECT 1 FROM shopify_api_queue q
+             WHERE q.request_hash = md5(($1 || $2) || COALESCE(($3::jsonb)::text,'')) AND q.status IN ('pending','paused'))`,
+          [qr.method, qr.endpoint, JSON.stringify(qr.payload), qr.token_alias, qr.priority, qr.status, qr.source_agent],
+        );
+        enq += r.rowCount;
+      }
+    }
+  }
+  await pool.end();
+  if (DIRECT) {
+    console.log(`\n✅ EXECUTE (--direct) done: mirror NULLed + ${delMf} metafields deleted + ${clrBc} barcode-clears, errors=${errs.length}. restore-map → ${execLog}`);
+    if (errs.length) console.log(errs.join('\n'));
+  } else {
+    console.log(`\n✅ EXECUTE done: mirror NULLed + ${enq} queue rows inserted. restore-map → ${execLog}`);
+    console.log('   NOTE: local queue has no runner — use --direct, or wire the Kamatera queue. Verify live before the next batch.');
+  }
+}
+
+main().catch(e => { console.error(e); process.exit(1); });

← dc85266 auto-data-snapshot: 2026-09-22T07:25:22 (4 data files) — scr  ·  back to Designerwallcoverings  ·  Make inventory-stamp-guard.mjs shim portable (TK-11786 revie f0f90e1 →