← back to Designerwallcoverings
budget.cjs: add finite 'stout' category (mirrors maharam TK-00060) so the Stout drip can't be starved by the backlog drain
7cbc2dfd39291251766c23b7f9dc5fe74a6daecb · 2026-07-27 16:35:45 -0700 · Steve Abrams
Files touched
M scripts/variant-budget/budget.cjs
Diff
commit 7cbc2dfd39291251766c23b7f9dc5fe74a6daecb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 27 16:35:45 2026 -0700
budget.cjs: add finite 'stout' category (mirrors maharam TK-00060) so the Stout drip can't be starved by the backlog drain
---
scripts/variant-budget/budget.cjs | 51 ++++++++++++++++++++++++++++++---------
1 file changed, 40 insertions(+), 11 deletions(-)
diff --git a/scripts/variant-budget/budget.cjs b/scripts/variant-budget/budget.cjs
index 834b201..8524573 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']);
+const VALID = new Set(['roll', 'sample', 'upload', 'osborne', 'backlog', 'maharam', 'stout']);
// '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
@@ -106,6 +106,14 @@ const OSB_OUT = path.join(REPO_ROOT, 'scripts', 'osborne-onboard', 'out');
const MAHARAM_CAP = parseInt(process.env.DW_MAHARAM_CAP || '100', 10); // 100 variants = 100 sample-only products/day
const MAHARAM_FLOOR = parseInt(process.env.DW_MAHARAM_FLOOR || String(MAHARAM_CAP), 10);
const MAH_OUT = path.join(REPO_ROOT, 'scripts', 'maharam-onboard', 'out');
+// 'stout' = the finite Stout Textiles wallcovering onboard (~128 remaining, 2 variants each:
+// Roll + $4.25 Sample). Modeled on 'maharam'/'osborne' — 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 bug it hit) NOR breach the ~1k/day cap.
+// REVERSIBLE — self-zeroes on completion; force off via DW_STOUT_CAP=0.
+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');
const today = () => {
const d = new Date();
@@ -143,8 +151,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 }
- : { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_CAP, maharam: MAHARAM_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 },
};
}
@@ -178,6 +186,21 @@ function maharamReservation(spentMaharam) {
return Math.min(Math.max(0, MAHARAM_FLOOR - spentMaharam), remainingProducts);
}
+// ── stout hard-floor reservation (finite; auto-reverts on completion) ──────────
+// Same shape as maharamReservation but 2 variants/product (Roll + Sample). The unspent floor,
+// never more than the ACTUAL remaining stout work. Auto-zeroes when created.jsonl reaches the
+// payload total → the reserved slice hands straight back to the pool (no calendar guess).
+function stoutReservation(spentStout) {
+ let remainingProducts = 0;
+ try {
+ const total = fs.readFileSync(path.join(STOUT_OUT, 'payloads.jsonl'), 'utf8').split('\n').filter(Boolean).length;
+ let created = 0;
+ try { created = fs.readFileSync(path.join(STOUT_OUT, 'created.jsonl'), 'utf8').split('\n').filter(Boolean).length; } catch (_) {}
+ remainingProducts = Math.max(0, total - created);
+ } catch (_) { return 0; } // no stout manifest → reserve nothing
+ return Math.min(Math.max(0, STOUT_FLOOR - spentStout), remainingProducts * 2);
+}
+
// ── mkdir-mutex (atomic create; stale-break after 10s) ─────────────────────────
function withLock(fn) {
fs.mkdirSync(LEDGER_DIR, { recursive: true });
@@ -201,11 +224,11 @@ function load() {
// backfill keys missing from pre-upload-category ledger files (avoid NaN arithmetic)
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.maharam = j.spent.maharam || 0; j.spent.stout = j.spent.stout || 0;
return j;
}
} catch (e) {}
- return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0, maharam: 0 } };
+ return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0, maharam: 0, stout: 0 } };
}
function save(j) {
const tmp = `${ledgerFile()}.tmp-${process.pid}`;
@@ -225,7 +248,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;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
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
@@ -235,6 +258,8 @@ function take(who, n) {
// what every OTHER category may draw, so the ~100/day trickle can't be starved. Maharam
// itself draws against the full remaining (it owns its reservation), exactly like osborne.
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));
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;
@@ -288,10 +313,11 @@ 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;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
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 === 'backlog' && reclaimActive()) return globalLeft;
return Math.min(Math.max(0, ph.caps[who] - j.spent[who]), globalLeft);
});
@@ -301,17 +327,19 @@ 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;
+ const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam + j.spent.stout;
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 othersGlobal = Math.max(0, globalLeft - osborneReserved - maharamReserved); // what roll/sample/upload/backlog may draw
+ 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
return {
date: j.date, budgetTotal: BUDGET_TOTAL,
phase: ph.rollPhase ? 'roll-priority' : 'backfill-priority',
rollBacklogRemaining: ph.rollBacklogRemaining, buildable: ph.buildable, rollsDone: ph.done,
osborneFloor: OSBORNE_FLOOR, osborneReserved,
maharamFloor: MAHARAM_FLOOR, maharamReserved,
+ stoutFloor: STOUT_FLOOR, stoutReserved,
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),
@@ -319,8 +347,9 @@ 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)),
- maharam: Math.min(Math.max(0, ph.caps.maharam - j.spent.maharam), Math.max(0, globalLeft - osborneReserved)),
+ 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)),
global: globalLeft },
};
}
← 039ce33 auto-save: 2026-07-27T16:21:58 (2 files) — scripts/sample-sp
·
back to Designerwallcoverings
·
auto-save: 2026-07-27T16:52:13 (2 files) — scripts/sample-sp e01df6e →