[object Object]

← 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

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 →