[object Object]

← back to Designerwallcoverings

Stroheim onboard: add settlement-gate + go-live (Steve-gated) + resume.sh — pipeline one-command-ready (DTD 5/5, yolo-plus)

07f745c4f5921a08be2927eb2daa9714f5b3b47b · 2026-07-26 16:46:06 -0700 · Steve Abrams

Files touched

Diff

commit 07f745c4f5921a08be2927eb2daa9714f5b3b47b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 16:46:06 2026 -0700

    Stroheim onboard: add settlement-gate + go-live (Steve-gated) + resume.sh — pipeline one-command-ready (DTD 5/5, yolo-plus)
---
 scripts/stroheim-onboard/README.md   |   3 +-
 scripts/stroheim-onboard/go-live.mjs | 153 +++++++++++++++++++++++++++++++++++
 scripts/stroheim-onboard/resume.sh   |  38 +++++++++
 3 files changed, 193 insertions(+), 1 deletion(-)

diff --git a/scripts/stroheim-onboard/README.md b/scripts/stroheim-onboard/README.md
index f8ecaed..fab3bcd 100644
--- a/scripts/stroheim-onboard/README.md
+++ b/scripts/stroheim-onboard/README.md
@@ -9,7 +9,8 @@ onboard pattern. **Prefix `DWST` (approved by Steve 2026-07-26).**
 - ✅ Staging complete: 782 rows, 100% images / width / specs, 0 "Unknown", all `net_new`,
   dedup-clean (0 Stroheim products on the live store).
 - ✅ PG tracking columns added: `stroheim_catalog.shopify_product_id`, `.on_shopify`.
-- ✅ Scripts built + proven: `build-payloads.mjs` reports **782 held no-price, 0 emitted**.
+- ✅ FULL pipeline built + proven (all syntax + dry-run clean): `mint-skus` · `build-payloads`
+  (782 held no-price, 0 emitted) · `settlement-gate` · `create-drafts` · `go-live` · `resume.sh`.
 - ⛔ **BLOCKED on PRICE** — `price_retail` is 100% NULL. Steve's hard rule forbids anything
   at $0/NULL. Nothing is created until price is sourced. (DTD 2026-07-26, 5/5 = HOLD.)
 
diff --git a/scripts/stroheim-onboard/go-live.mjs b/scripts/stroheim-onboard/go-live.mjs
new file mode 100644
index 0000000..8a76d3e
--- /dev/null
+++ b/scripts/stroheim-onboard/go-live.mjs
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+/**
+ * Go-live (GATED trickle) for the Stroheim drafts created by create-drafts.mjs.
+ *
+ * ⚠️ THIS IS THE STEVE-GATED ACTIVATION STEP — it makes products customer-facing.
+ * It is NEVER auto-run by the yolo loop and is NOT scheduled. Run only on Steve's explicit go.
+ *
+ * For each draft (out/created.jsonl) this:
+ *   1. SETTLEMENT re-gate — activate only if out/settlement-verdicts.jsonl verdict is OK.
+ *   2. validateBeforeActivate (5/6-field) — width, description, ≥1 image, title guards,
+ *      sample variant. On FAIL: keep DRAFT, add Needs-* tags, record in out/held.jsonl, skip.
+ *   3. Enable inventory tracking + activate at Ventura Blvd + on_hand=2026 on BOTH variants
+ *      (roll + $4.25 sample) so the memo AND the roll are orderable.
+ *   4. Publish to the 12 DW sales channels — Google & YouTube EXCLUDED (Steve's rule +
+ *      GMC $4.25-leak protection; the sample variant would otherwise disapprove).
+ *   5. status = ACTIVE. vendor='Stroheim' → the `stroheim-wallcoverings` collection fills.
+ *   6. PG-first write-back: shopify_product_id + on_shopify on stroheim_catalog (keyed on
+ *      proposed_dw_sku; NOTE stroheim_catalog has no updated_at column), and flip
+ *      dw_sku_registry → active.
+ * Idempotent: skips products already ACTIVE or already in golive-done.jsonl.
+ *
+ *   node go-live.mjs                   # dry-run summary
+ *   node go-live.mjs --apply --limit=2 # canary
+ *   node go-live.mjs --apply --limit=40 # trickle N/tick (metered cadence)
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
+import { execFileSync } from 'node:child_process';
+import { gql } from '../lib/shopify.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const OUT = path.join(HERE, 'out');
+const require = createRequire(import.meta.url);
+const { validateBeforeActivate } = require('/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js');
+const APPLY = process.argv.includes('--apply');
+const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
+const PGENV = { ...process.env, PGHOST: '/tmp', PGDATABASE: 'dw_unified' };
+
+const LOCATION_ID = 'gid://shopify/Location/5795643504'; // 15442 Ventura Blvd.
+const TARGET_QTY = 2026;
+// Google & YouTube (29646651457) EXCLUDED — the $4.25 sample variant → GMC price disapproval.
+const PUBLICATIONS = [
+  22208643184, 22497296496, 29739483201, 29776969793, 37904089153,
+  43657658419, 44234276915, 44317474867, 44317507635, 71898464307, 115856375859, 140027723827,
+].map(n => `gid://shopify/Publication/${n}`);
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
+const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
+const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
+const M_PUBLISH = `mutation($id:ID!,$pubs:[PublicationInput!]!){ publishablePublish(id:$id, input:$pubs){ userErrors{message} } }`;
+const M_ACTIVE = `mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{status} userErrors{message} } }`;
+const M_TAGS = `mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id, tags:$tags){ userErrors{message} } }`;
+
+const loadJsonl = file => {
+  const p = path.join(OUT, file);
+  return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l)) : [];
+};
+
+function dbWriteBack(sku, mfrSku, pid, handle) {
+  const q = sku.replace(/'/g, "''"), mq = String(mfrSku || '').replace(/'/g, "''");
+  const h = handle ? `'${handle.replace(/'/g, "''")}'` : 'NULL';
+  try {
+    execFileSync(PSQL, ['-d', 'dw_unified', '-c',
+      `UPDATE stroheim_catalog SET shopify_product_id='${pid}', on_shopify=true WHERE proposed_dw_sku='${q}';
+       INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku, shopify_product_id, shopify_handle, status, min_order_qty, order_increment, created_at, updated_at)
+       VALUES ('${q}','DWST','Stroheim','${mq}','${pid}',${h},'active',1,1,NOW(),NOW())
+       ON CONFLICT (dw_sku) DO UPDATE SET shopify_product_id=EXCLUDED.shopify_product_id, shopify_handle=COALESCE(EXCLUDED.shopify_handle, dw_sku_registry.shopify_handle), status='active', updated_at=NOW();`],
+      { env: PGENV, stdio: 'ignore' });
+  } catch (e) { /* non-fatal — Shopify write is authoritative; mirror re-syncs */ }
+}
+
+function validateShape(o) {
+  const mf = (ns, k) => { const m = (o.metafields || []).find(x => x.namespace === ns && x.key === k); return m ? m.value : ''; };
+  const specs = { width: mf('global', 'width'), repeat: mf('global', 'repeat'),
+    material: mf('custom', 'material'), unitOfMeasure: mf('global', 'unit_of_measure') };
+  return {
+    title: o.product.title, vendor: o.product.vendor, tags: o.product.tags, dwSku: o.sku,
+    descriptionHtml: o.product.body_html,
+    specs, vendorSpecs: specs,
+    images: o.gallery, vendorImages: o.gallery,
+    variants: o.product.variants,
+  };
+}
+
+async function goLive(pid, sku, mfrSku) {
+  const gidP = `gid://shopify/Product/${pid}`;
+  const errs = [];
+  const d = await gql(Q_VARIANTS, { id: gidP });
+  if (!d?.product) return { missing: true };
+  if (d.product.status === 'ACTIVE') { dbWriteBack(sku, mfrSku, pid, d.product.handle); return { skipped: true }; }
+  const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+  for (const iid of items) {
+    let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
+    r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
+    (r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
+  }
+  const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
+    quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+  (r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
+  const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.map(p => ({ publicationId: p })) });
+  (r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
+  const r4 = await gql(M_ACTIVE, { id: gidP });
+  (r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
+  const status = r4.productUpdate?.product?.status;
+  if (status === 'ACTIVE') dbWriteBack(sku, mfrSku, pid, d.product.handle);
+  return { status, errs };
+}
+
+(async () => {
+  const done = new Set(loadJsonl('golive-done.jsonl').map(r => r.sku));
+  const heldAlready = new Set(loadJsonl('held.jsonl').map(r => r.sku));
+  const verdict = new Map(loadJsonl('settlement-verdicts.jsonl').map(r => [r.sku.toUpperCase(), r.verdict]));
+  const payload = new Map(loadJsonl('payloads.jsonl').map(o => [o.sku.toUpperCase(), o]));
+  let todo = loadJsonl('created.jsonl').filter(r => !done.has(r.sku));
+  if (LIMIT) todo = todo.slice(0, LIMIT);
+  console.log(`go-live: ${todo.length} drafts · loc ${LOCATION_ID} · qty ${TARGET_QTY} · ${PUBLICATIONS.length} channels (Google excluded) · ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+  if (!APPLY) { console.log('first 3:', todo.slice(0, 3).map(r => r.sku).join(', ')); console.log('DRY-RUN. --apply to execute (Steve-gated activation).'); return; }
+
+  const fdDone = fs.openSync(path.join(OUT, 'golive-done.jsonl'), 'a');
+  const fdHeld = fs.openSync(path.join(OUT, 'held.jsonl'), 'a');
+  let ok = 0, skip = 0, withErr = 0, miss = 0, held = 0;
+  for (const r of todo) {
+    try {
+      const gidP = `gid://shopify/Product/${r.product_id}`;
+      if (verdict.get(r.sku.toUpperCase()) !== 'OK') {
+        if (!heldAlready.has(r.sku)) { fs.writeSync(fdHeld, JSON.stringify({ sku: r.sku, product_id: r.product_id, reason: `settlement:${verdict.get(r.sku.toUpperCase()) || 'ungated'}` }) + '\n'); heldAlready.add(r.sku); }
+        held++; console.error(`  ⏸ HOLD ${r.sku}: settlement ${verdict.get(r.sku.toUpperCase()) || 'ungated'}`); continue;
+      }
+      const o = payload.get(r.sku.toUpperCase());
+      const gate = o ? validateBeforeActivate(validateShape(o)) : { ok: false, reasons: ['no payload'], tags: [] };
+      if (!gate.ok) {
+        if (gate.tags.length) await gql(M_TAGS, { id: gidP, tags: gate.tags });
+        if (!heldAlready.has(r.sku)) { fs.writeSync(fdHeld, JSON.stringify({ sku: r.sku, product_id: r.product_id, reason: gate.reasons.join('; '), tags: gate.tags }) + '\n'); heldAlready.add(r.sku); }
+        held++; console.error(`  ⏸ HOLD ${r.sku}: ${gate.reasons.join('; ')}`); continue;
+      }
+      const res = await goLive(r.product_id, r.sku, r.mfr_sku);
+      if (res.missing) { miss++; console.error(`  ? ${r.sku}: product not found`); }
+      else if (res.skipped) skip++;
+      else if (res.errs?.length) { withErr++; console.error(`  ⚠ ${r.sku} ${res.status || '?'}: ${res.errs.slice(0, 3).join(' | ')}`); }
+      else { ok++; }
+      if (res.status === 'ACTIVE' || res.skipped) fs.writeSync(fdDone, JSON.stringify({ sku: r.sku, product_id: r.product_id }) + '\n');
+      if ((ok + skip + withErr + miss + held) % 20 === 0) console.log(`  ...${ok + skip + withErr + miss + held}/${todo.length} (active=${ok} skip=${skip} held=${held} err=${withErr} miss=${miss})`);
+      await sleep(350);
+    } catch (e) { withErr++; console.error(`  ERR ${r.sku}: ${String(e).slice(0, 160)}`); await sleep(600); }
+  }
+  fs.closeSync(fdDone); fs.closeSync(fdHeld);
+  console.log(`\nDONE. active=${ok} skipped=${skip} held=${held} with-errors=${withErr} missing=${miss} of ${todo.length}`);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/stroheim-onboard/resume.sh b/scripts/stroheim-onboard/resume.sh
new file mode 100755
index 0000000..2900caf
--- /dev/null
+++ b/scripts/stroheim-onboard/resume.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Stroheim onboard — one-command run order (idempotent, cap-clean).
+# Modeled on muralsource-onboard/resume.sh, BUT activation is OPT-IN and OFF by default:
+# this staging-first onboard produces DRAFTS only unless STROHEIM_ALLOW_GOLIVE=1 is set.
+#
+# One tick =
+#   1. mint-skus       — assign DWST-810001+ to priced+imaged net_new rows (staging only)
+#   2. build-payloads  — build the plan from stroheim_catalog (skips no-price/no-image/unminted)
+#   3. settlement-gate — Gemini-vision gate any ungated image (memo #3; metered; $0 if 0 payloads)
+#   4. create-drafts   — mint settlement-OK drafts as DRAFT (shared 'backlog' variant budget)
+#   5. go-live         — ONLY if STROHEIM_ALLOW_GOLIVE=1 (Steve-gated activation; channels
+#                        except Google/YouTube). Default: SKIPPED — drafts stay drafts.
+#
+# HARD BLOCK: while price_retail is NULL, steps 1-4 no-op cleanly (build emits 0). This script
+# is NOT scheduled in launchd (scheduled-job install is itself gated) — run it by hand.
+set -uo pipefail
+cd "$(dirname "$0")" || exit 1
+export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
+NODE="$(command -v node)"
+GOLIVE_LIMIT="${DW_STROHEIM_GOLIVE_LIMIT:-40}"    # metered cadence — activations per tick
+ts() { date '+%Y-%m-%dT%H:%M:%S'; }
+
+echo "===== $(ts) stroheim onboard tick ====="
+"$NODE" mint-skus.mjs --apply
+"$NODE" build-payloads.mjs
+"$NODE" settlement-gate.mjs --apply
+"$NODE" create-drafts.mjs --apply
+
+if [ "${STROHEIM_ALLOW_GOLIVE:-0}" = "1" ]; then
+  echo "$(ts) STROHEIM_ALLOW_GOLIVE=1 → running gated activation (limit ${GOLIVE_LIMIT})"
+  "$NODE" go-live.mjs --apply --limit="${GOLIVE_LIMIT}"
+else
+  echo "$(ts) go-live SKIPPED (drafts-only). Set STROHEIM_ALLOW_GOLIVE=1 to activate — Steve-gated."
+fi
+
+CREATED=$(wc -l < out/created.jsonl 2>/dev/null | tr -d ' ' || echo 0)
+echo "$(ts) state: created=${CREATED}"
+echo "===== $(ts) tick done ====="

← 1000bbf auto-save: 2026-07-26T16:44:36 (215 files) — scripts/sample-  ·  back to Designerwallcoverings  ·  Stroheim onboard: read-only price/coverage canary (auto-resu eb3b887 →