← back to Designerwallcoverings
stroheim onboard (TK-00039): dedicated 'stroheim' variant-budget lane + create-drafts wiring
4eaf385ec4df8b1a1620a601fe7209abdc7eb863 · 2026-07-27 18:09:58 -0700 · Steve Abrams
- budget.cjs: add finite 'stroheim' category modeled on stout (own ~100/day floor,
auto-zeroes on completion, clamped to the ~1000/day Shopify cap). Contrarian-gated (SHIP IT).
- create-drafts.mjs: BUDGET_CAT default backlog->stroheim (stroheim-scoped; verified no other
caller). settlement-gate.mjs: retry transient img-fetch + SKIP (not fail-closed BLOCK) on
fetch/vision error. resume.sh: note dedicated lane. Add (uninstalled, Steve-gated) launchd plist.
Prices were already in canonical dw_unified (771/782 roll retail); local mirror was stale.
771 minted DWST-810001..810771, 771 payloads built. Drafts trickle once budget frees (gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A scripts/stroheim-onboard/com.steve.stroheim-onboard-resume.plistM scripts/stroheim-onboard/create-drafts.mjsM scripts/stroheim-onboard/resume.shM scripts/stroheim-onboard/settlement-gate.mjsM scripts/variant-budget/budget.cjs
Diff
commit 4eaf385ec4df8b1a1620a601fe7209abdc7eb863
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 27 18:09:58 2026 -0700
stroheim onboard (TK-00039): dedicated 'stroheim' variant-budget lane + create-drafts wiring
- budget.cjs: add finite 'stroheim' category modeled on stout (own ~100/day floor,
auto-zeroes on completion, clamped to the ~1000/day Shopify cap). Contrarian-gated (SHIP IT).
- create-drafts.mjs: BUDGET_CAT default backlog->stroheim (stroheim-scoped; verified no other
caller). settlement-gate.mjs: retry transient img-fetch + SKIP (not fail-closed BLOCK) on
fetch/vision error. resume.sh: note dedicated lane. Add (uninstalled, Steve-gated) launchd plist.
Prices were already in canonical dw_unified (771/782 roll retail); local mirror was stale.
771 minted DWST-810001..810771, 771 payloads built. Drafts trickle once budget frees (gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
.../com.steve.stroheim-onboard-resume.plist | 27 +++++++++++
scripts/stroheim-onboard/create-drafts.mjs | 2 +-
scripts/stroheim-onboard/resume.sh | 3 +-
scripts/stroheim-onboard/settlement-gate.mjs | 32 +++++++------
scripts/variant-budget/budget.cjs | 53 +++++++++++++++++-----
5 files changed, 91 insertions(+), 26 deletions(-)
diff --git a/scripts/stroheim-onboard/com.steve.stroheim-onboard-resume.plist b/scripts/stroheim-onboard/com.steve.stroheim-onboard-resume.plist
new file mode 100644
index 0000000..3eff8ee
--- /dev/null
+++ b/scripts/stroheim-onboard/com.steve.stroheim-onboard-resume.plist
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key><string>com.steve.stroheim-onboard-resume</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/bin/bash</string>
+ <string>/Users/macstudio3/Projects/designerwallcoverings/scripts/stroheim-onboard/resume.sh</string>
+ </array>
+ <!-- Trickle the 771 settlement-OK Stroheim (DWST-810xxx) DRAFTS under the DEDICATED 'stroheim'
+ variant slice (own floor ~100/day = ~50 products/day, ~16 days; NEVER 'upload'). resume.sh
+ is DRAFTS-ONLY (STROHEIM_ALLOW_GOLIVE unset → go-live SKIPPED) — activation stays a
+ separate Steve-gated step. 00:40 primary + 13:40 retry (offset from stout 00:30/13:30,
+ muralsource 00:20/13:20, greenland 00:10/13:00 to avoid backlog contention). resume.sh is
+ idempotent + settlement-gated + cap-clean and no-ops once every OK SKU is created.
+ INSTALL (Steve-gated — scheduled-job install). To arm:
+ cp ~/Projects/designerwallcoverings/scripts/stroheim-onboard/com.steve.stroheim-onboard-resume.plist ~/Library/LaunchAgents/
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.steve.stroheim-onboard-resume.plist -->
+ <key>StartCalendarInterval</key>
+ <array>
+ <dict><key>Hour</key><integer>0</integer><key>Minute</key><integer>40</integer></dict>
+ <dict><key>Hour</key><integer>13</integer><key>Minute</key><integer>40</integer></dict>
+ </array>
+ <key>StandardOutPath</key><string>/Users/macstudio3/Projects/designerwallcoverings/scripts/stroheim-onboard/out/launchd.out</string>
+ <key>StandardErrorPath</key><string>/Users/macstudio3/Projects/designerwallcoverings/scripts/stroheim-onboard/out/launchd.err</string>
+</dict></plist>
diff --git a/scripts/stroheim-onboard/create-drafts.mjs b/scripts/stroheim-onboard/create-drafts.mjs
index e6916b7..23a649a 100644
--- a/scripts/stroheim-onboard/create-drafts.mjs
+++ b/scripts/stroheim-onboard/create-drafts.mjs
@@ -30,7 +30,7 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
const require = createRequire(import.meta.url);
const budget = require('../variant-budget/budget.cjs');
-const BUDGET_CAT = process.env.DW_STROHEIM_BUDGET || 'backlog'; // SHARED backlog budget — never 'upload'
+const BUDGET_CAT = process.env.DW_STROHEIM_BUDGET || 'stroheim'; // dedicated finite-onboard category (TK-00039) — floor-protected, never 'upload'
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';
diff --git a/scripts/stroheim-onboard/resume.sh b/scripts/stroheim-onboard/resume.sh
index ecb55f9..4eaf95b 100755
--- a/scripts/stroheim-onboard/resume.sh
+++ b/scripts/stroheim-onboard/resume.sh
@@ -7,7 +7,8 @@
# 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)
+# 4. create-drafts — mint settlement-OK drafts as DRAFT (DEDICATED 'stroheim' variant budget,
+# own ~100/day floor, TK-00039; never 'upload'/'backlog')
# 5. go-live — ONLY if STROHEIM_ALLOW_GOLIVE=1 (Steve-gated activation; channels
# except Google/YouTube). Default: SKIPPED — drafts stay drafts.
#
diff --git a/scripts/stroheim-onboard/settlement-gate.mjs b/scripts/stroheim-onboard/settlement-gate.mjs
index ea16afa..7e3b350 100644
--- a/scripts/stroheim-onboard/settlement-gate.mjs
+++ b/scripts/stroheim-onboard/settlement-gate.mjs
@@ -51,11 +51,15 @@ Respond with ONLY this JSON, booleans true/false (never null), plus a 3-6 word e
{"a1":bool,"a2":bool,"a3":bool,"b":bool,"acceptable":bool,"evidence":{"a1":"","a2":"","a3":"","b":"","acceptable":""}}`;
async function fetchB64(url) {
- const r = await fetch(url, { signal: AbortSignal.timeout(30000) });
- if (!r.ok) throw new Error(`img ${r.status}`);
- const ct = r.headers.get('content-type') || 'image/jpeg';
- const buf = Buffer.from(await r.arrayBuffer());
- return { mime: ct.split(';')[0], data: buf.toString('base64') };
+ for (let t = 0; t < 4; t++) {
+ try {
+ const r = await fetch(url, { signal: AbortSignal.timeout(30000) });
+ if (!r.ok) throw new Error(`img ${r.status}`);
+ const ct = r.headers.get('content-type') || 'image/jpeg';
+ const buf = Buffer.from(await r.arrayBuffer());
+ return { mime: ct.split(';')[0], data: buf.toString('base64') };
+ } catch (e) { if (t === 3) throw e; await sleep(800 * (t + 1)); } // retry transient img-fetch (ECONNRESET/timeout)
+ }
}
async function gemini(b64) {
@@ -98,20 +102,22 @@ async function main() {
if (!APPLY) { console.log(` first 3: ${todo.slice(0, 3).map(p => p.sku).join(', ')}`); console.log('DRY-RUN. --apply to gate.'); return; }
const fd = fs.openSync(path.join(OUT, 'settlement-verdicts.jsonl'), 'a');
- let ok = 0, block = 0, err = 0, spent = 0;
+ let ok = 0, block = 0, skip = 0, spent = 0;
for (const p of todo) {
- let rec;
- try {
- const b64 = await fetchB64(p.image_url);
- const v = await gemini(b64); spent += COST_PER_IMG;
- rec = { sku: p.sku, ...verdict(v), gated_at: new Date().toISOString() };
- } catch (e) { rec = { sku: p.sku, verdict: 'BLOCK', reason: `error: ${String(e).slice(0, 60)} → fail closed`, gated_at: new Date().toISOString() }; err++; }
+ let b64;
+ try { b64 = await fetchB64(p.image_url); }
+ catch (e) { skip++; await sleep(400); continue; } // img-fetch failed after retries → skip, retry next pass
+ const v = await gemini(b64); spent += COST_PER_IMG;
+ const hasAll = v && ['a1', 'a2', 'a3', 'b'].every(k => typeof v[k] === 'boolean');
+ if (!hasAll) { skip++; await sleep(400); continue; } // vision null/malformed → SKIP (not BLOCK), retry next pass
+ const rec = { sku: p.sku, ...verdict(v), gated_at: new Date().toISOString() };
fs.writeSync(fd, JSON.stringify(rec) + '\n');
if (rec.verdict === 'OK') ok++; else block++;
- if ((ok + block) % 20 === 0) console.log(` ...${ok + block}/${todo.length} (OK ${ok}, BLOCK ${block}, err ${err}) · $${spent.toFixed(4)}`);
+ if ((ok + block + skip) % 20 === 0) console.log(` ...${ok + block + skip}/${todo.length} (OK ${ok}, BLOCK ${block}, skip ${skip}) · $${spent.toFixed(4)}`);
await sleep(400);
}
fs.closeSync(fd);
+ console.log(` pass result: OK ${ok} · BLOCK ${block} · skipped(retry-next-pass) ${skip}`);
console.log(`\nDONE. OK=${ok} BLOCK=${block} err=${err} of ${todo.length} · actual ~$${spent.toFixed(4)} (Gemini ${MODEL} vision)`);
if (block) console.log(` ⚠ ${block} BLOCK/held — will NOT be created/activated; review out/settlement-verdicts.jsonl (grep BLOCK).`);
}
diff --git a/scripts/variant-budget/budget.cjs b/scripts/variant-budget/budget.cjs
index 8524573..5d4a242 100644
--- a/scripts/variant-budget/budget.cjs
+++ b/scripts/variant-budget/budget.cjs
@@ -53,7 +53,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..'); // designerwall
const ROLL_DONE = path.join(REPO_ROOT, 'roll-scale-done2.txt'); // build-roll-scale.js resume file
const PGENV = { PGHOST: '/tmp', PGPORT: '5432', PGUSER: 'stevestudio2', PGDATABASE: 'dw_unified',
PATH: '/opt/homebrew/opt/postgresql@14/bin:/usr/local/bin:/usr/bin:/bin' };
-const VALID = new Set(['roll', 'sample', 'upload', 'osborne', 'backlog', 'maharam', 'stout']);
+const VALID = new Set(['roll', 'sample', 'upload', 'osborne', 'backlog', 'maharam', 'stout', 'stroheim']);
// 'upload' = the all-day new-product uploader (cadence-import). Modest 200/day slice
// carved from the shared 800 ceiling via competition (NOT a hard reservation): on light
// days it's free leftover; on max-capacity days it trims roll/sample slightly. Keeps new
@@ -114,6 +114,16 @@ const MAH_OUT = path.join(REPO_ROOT, 'scripts', 'maharam-onboard', 'out');
const STOUT_CAP = parseInt(process.env.DW_STOUT_CAP || '100', 10);
const STOUT_FLOOR = parseInt(process.env.DW_STOUT_FLOOR || String(STOUT_CAP), 10);
const STOUT_OUT = path.join(REPO_ROOT, 'scripts', 'stout-onboard', 'out');
+// 'stroheim' = the finite Stroheim wallcovering onboard (771 priced products, 2 variants each:
+// Roll + $4.25 Sample). TK-00039 (Steve 2026-07-27, "Own 'stroheim' floor like stout"): modeled
+// on 'stout' — its OWN category + a floor reservation (below) that auto-zeroes when created.jsonl
+// reaches the payload total, so the ~100/day trickle can't be starved by the 5-field 'backlog'
+// drain (the slice it was previously sharing) NOR breach the ~1,000/day Shopify cap (clamped to
+// remainingGlobal like every category). 771×2=1,542 variants → ~16 days.
+// REVERSIBLE — self-zeroes on completion; force off via DW_STROHEIM_CAP=0.
+const STROHEIM_CAP = parseInt(process.env.DW_STROHEIM_CAP || '100', 10);
+const STROHEIM_FLOOR = parseInt(process.env.DW_STROHEIM_FLOOR || String(STROHEIM_CAP), 10);
+const STROHEIM_OUT = path.join(REPO_ROOT, 'scripts', 'stroheim-onboard', 'out');
const today = () => {
const d = new Date();
@@ -151,8 +161,8 @@ function phase() {
// slice is never pre-empted. osborne keeps its small finite floor. REVERSIBLE — restore
// roll/sample 400/80 (rollPhase) · 250/120 (backfill) when the new-SKU push goal is met.
caps: rollPhase
- ? { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_CAP, maharam: MAHARAM_CAP, stout: STOUT_CAP }
- : { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_CAP, maharam: MAHARAM_CAP, stout: STOUT_CAP },
+ ? { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_CAP, maharam: MAHARAM_CAP, stout: STOUT_CAP, stroheim: STROHEIM_CAP }
+ : { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_CAP, maharam: MAHARAM_CAP, stout: STOUT_CAP, stroheim: STROHEIM_CAP },
};
}
@@ -201,6 +211,21 @@ function stoutReservation(spentStout) {
return Math.min(Math.max(0, STOUT_FLOOR - spentStout), remainingProducts * 2);
}
+// ── stroheim hard-floor reservation (finite; auto-reverts on completion) ───────
+// Same shape as stoutReservation — 2 variants/product (Roll + Sample). The unspent floor, never
+// more than the ACTUAL remaining stroheim work. Auto-zeroes when created.jsonl reaches the payload
+// total → the reserved slice hands straight back to the pool (no calendar guess).
+function stroheimReservation(spentStroheim) {
+ let remainingProducts = 0;
+ try {
+ const total = fs.readFileSync(path.join(STROHEIM_OUT, 'payloads.jsonl'), 'utf8').split('\n').filter(Boolean).length;
+ let created = 0;
+ try { created = fs.readFileSync(path.join(STROHEIM_OUT, 'created.jsonl'), 'utf8').split('\n').filter(Boolean).length; } catch (_) {}
+ remainingProducts = Math.max(0, total - created);
+ } catch (_) { return 0; } // no stroheim manifest → reserve nothing
+ return Math.min(Math.max(0, STROHEIM_FLOOR - spentStroheim), remainingProducts * 2);
+}
+
// ── mkdir-mutex (atomic create; stale-break after 10s) ─────────────────────────
function withLock(fn) {
fs.mkdirSync(LEDGER_DIR, { recursive: true });
@@ -225,10 +250,11 @@ function load() {
j.spent.roll = j.spent.roll || 0; j.spent.sample = j.spent.sample || 0; j.spent.upload = j.spent.upload || 0;
j.spent.osborne = j.spent.osborne || 0; j.spent.backlog = j.spent.backlog || 0;
j.spent.maharam = j.spent.maharam || 0; j.spent.stout = j.spent.stout || 0;
+ j.spent.stroheim = j.spent.stroheim || 0;
return j;
}
} catch (e) {}
- return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0, maharam: 0, stout: 0 } };
+ return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0, maharam: 0, stout: 0, stroheim: 0 } };
}
function save(j) {
const tmp = `${ledgerFile()}.tmp-${process.pid}`;
@@ -248,7 +274,7 @@ function take(who, n) {
return withLock(() => {
const ph = phase();
const j = load();
- const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout + j.spent.stroheim;
let remainingGlobal = Math.max(0, BUDGET_TOTAL - spentTotal);
// Hard floor: subtract osborne's unspent reserved slice from what every OTHER category may
// draw, so roll/sample/upload can't starve the finite Osborne onboard. Osborne itself draws
@@ -260,6 +286,7 @@ function take(who, n) {
if (who !== 'maharam') remainingGlobal = Math.max(0, remainingGlobal - maharamReservation(j.spent.maharam));
// Same finite-floor protection for stout (owns its reservation, exactly like osborne/maharam).
if (who !== 'stout') remainingGlobal = Math.max(0, remainingGlobal - stoutReservation(j.spent.stout));
+ if (who !== 'stroheim') remainingGlobal = Math.max(0, remainingGlobal - stroheimReservation(j.spent.stroheim));
let remainingWho = Math.max(0, ph.caps[who] - j.spent[who]);
// end-of-day reclaim: backlog may sweep the global unspent remainder in late slots
if (who === 'backlog' && reclaimActive()) remainingWho = remainingGlobal;
@@ -313,11 +340,12 @@ function remaining(who) {
return withLock(() => {
const ph = phase();
const j = load();
- const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout + j.spent.stroheim;
let globalLeft = Math.max(0, BUDGET_TOTAL - spentTotal);
if (who !== 'osborne') globalLeft = Math.max(0, globalLeft - osborneReservation(j.spent.osborne));
if (who !== 'maharam') globalLeft = Math.max(0, globalLeft - maharamReservation(j.spent.maharam));
if (who !== 'stout') globalLeft = Math.max(0, globalLeft - stoutReservation(j.spent.stout));
+ if (who !== 'stroheim') globalLeft = Math.max(0, globalLeft - stroheimReservation(j.spent.stroheim));
if (who === 'backlog' && reclaimActive()) return globalLeft;
return Math.min(Math.max(0, ph.caps[who] - j.spent[who]), globalLeft);
});
@@ -327,12 +355,13 @@ function remaining(who) {
function status() {
const ph = phase();
const j = load();
- const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout + j.spent.stroheim;
const globalLeft = Math.max(0, BUDGET_TOTAL - spentTotal);
const osborneReserved = osborneReservation(j.spent.osborne); // unspent hard floor (auto-zeroes on completion)
const maharamReserved = maharamReservation(j.spent.maharam); // finite Maharam floor (auto-zeroes on completion)
const stoutReserved = stoutReservation(j.spent.stout); // finite Stout floor (auto-zeroes on completion)
- const othersGlobal = Math.max(0, globalLeft - osborneReserved - maharamReserved - stoutReserved); // what roll/sample/upload/backlog may draw
+ const stroheimReserved = stroheimReservation(j.spent.stroheim); // finite Stroheim floor (auto-zeroes on completion)
+ const othersGlobal = Math.max(0, globalLeft - osborneReserved - maharamReserved - stoutReserved - stroheimReserved); // what roll/sample/upload/backlog may draw
return {
date: j.date, budgetTotal: BUDGET_TOTAL,
phase: ph.rollPhase ? 'roll-priority' : 'backfill-priority',
@@ -340,6 +369,7 @@ function status() {
osborneFloor: OSBORNE_FLOOR, osborneReserved,
maharamFloor: MAHARAM_FLOOR, maharamReserved,
stoutFloor: STOUT_FLOOR, stoutReserved,
+ stroheimFloor: STROHEIM_FLOOR, stroheimReserved,
backlogReclaim: { hour: RECLAIM_HOUR, active: reclaimActive() },
caps: ph.caps, spent: j.spent, spentTotal,
remaining: { roll: Math.min(Math.max(0, ph.caps.roll - j.spent.roll), othersGlobal),
@@ -347,9 +377,10 @@ function status() {
upload: Math.min(Math.max(0, ph.caps.upload - j.spent.upload), othersGlobal),
backlog: reclaimActive() ? othersGlobal
: Math.min(Math.max(0, ph.caps.backlog - j.spent.backlog), othersGlobal),
- osborne: Math.min(Math.max(0, ph.caps.osborne - j.spent.osborne), Math.max(0, globalLeft - maharamReserved - stoutReserved)),
- maharam: Math.min(Math.max(0, ph.caps.maharam - j.spent.maharam), Math.max(0, globalLeft - osborneReserved - stoutReserved)),
- stout: Math.min(Math.max(0, ph.caps.stout - j.spent.stout), Math.max(0, globalLeft - osborneReserved - maharamReserved)),
+ osborne: Math.min(Math.max(0, ph.caps.osborne - j.spent.osborne), Math.max(0, globalLeft - maharamReserved - stoutReserved - stroheimReserved)),
+ maharam: Math.min(Math.max(0, ph.caps.maharam - j.spent.maharam), Math.max(0, globalLeft - osborneReserved - stoutReserved - stroheimReserved)),
+ stout: Math.min(Math.max(0, ph.caps.stout - j.spent.stout), Math.max(0, globalLeft - osborneReserved - maharamReserved - stroheimReserved)),
+ stroheim: Math.min(Math.max(0, ph.caps.stroheim - j.spent.stroheim), Math.max(0, globalLeft - osborneReserved - maharamReserved - stoutReserved)),
global: globalLeft },
};
}
← b6f54b1 auto-save: 2026-07-27T17:52:34 (5 files) — scripts/sample-sp
·
back to Designerwallcoverings
·
chore: v0.1.3 → v0.1.4 (session close — stroheim budget lane 058a362 →