[object Object]

← back to Designerwallcoverings

TK-00060: give Maharam onboard its own finite 'maharam' variant-budget category (DTD-A)

0034812642fbfa6844e29865e71353f78f5bb53f · 2026-07-27 16:01:36 -0700 · Steve Abrams

The daily Maharam drip drew from shared 'backlog' (cap 116/day), fully consumed by
the 5-field activation drain before the 05:30 tick — so it granted 0 drafts/day and
1,429 sample-only wallcoverings never onboarded. Add a dedicated 'maharam' category
mirroring the proven finite 'osborne' pattern: cap 100/day (DW_MAHARAM_CAP) +
maharamReservation() that auto-zeroes when created.jsonl reaches the payload total,
clamped to remainingGlobal so it can never breach Shopify's ~1000/day cap nor starve
upload/backlog/osborne. Point create-drafts.mjs default at 'maharam'. Also derive the
budget.take/refund/remaining error strings from VALID so they can't go stale (contrarian).

Verified: node --check + bash -n pass; status shows maharam cap100/reserved100/remaining100
(upload untouched, only residual trims 248->148); net-zero take/refund respects the 100 cap;
auto-revert proven (created=1429 -> reserved 0). Ledger left clean. Live draft writes remain
gated to the pre-approved 05:30 cron. Gated: DTD 5/5, contrarian SHIP IT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 0034812642fbfa6844e29865e71353f78f5bb53f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 16:01:36 2026 -0700

    TK-00060: give Maharam onboard its own finite 'maharam' variant-budget category (DTD-A)
    
    The daily Maharam drip drew from shared 'backlog' (cap 116/day), fully consumed by
    the 5-field activation drain before the 05:30 tick — so it granted 0 drafts/day and
    1,429 sample-only wallcoverings never onboarded. Add a dedicated 'maharam' category
    mirroring the proven finite 'osborne' pattern: cap 100/day (DW_MAHARAM_CAP) +
    maharamReservation() that auto-zeroes when created.jsonl reaches the payload total,
    clamped to remainingGlobal so it can never breach Shopify's ~1000/day cap nor starve
    upload/backlog/osborne. Point create-drafts.mjs default at 'maharam'. Also derive the
    budget.take/refund/remaining error strings from VALID so they can't go stale (contrarian).
    
    Verified: node --check + bash -n pass; status shows maharam cap100/reserved100/remaining100
    (upload untouched, only residual trims 248->148); net-zero take/refund respects the 100 cap;
    auto-revert proven (created=1429 -> reserved 0). Ledger left clean. Live draft writes remain
    gated to the pre-approved 05:30 cron. Gated: DTD 5/5, contrarian SHIP IT.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 scripts/maharam-onboard/create-drafts.mjs     |  7 ++--
 scripts/maharam-onboard/maharam-daily-drip.sh |  8 ++--
 scripts/variant-budget/budget.cjs             | 55 +++++++++++++++++++++------
 3 files changed, 53 insertions(+), 17 deletions(-)

diff --git a/scripts/maharam-onboard/create-drafts.mjs b/scripts/maharam-onboard/create-drafts.mjs
index 906eae1..667cfe0 100644
--- a/scripts/maharam-onboard/create-drafts.mjs
+++ b/scripts/maharam-onboard/create-drafts.mjs
@@ -10,8 +10,9 @@
  *   node create-drafts.mjs --apply             # all remaining (resumable, day-cap aware)
  *
  * Idempotent: skips any DWAX sku already live on Shopify OR already in out/created.jsonl.
- * Cap-aware: draws from the shared daily variant budget (default 'backlog' trickle pool,
- *   NOT 'upload' — so it never starves the new-arrivals uploader). Override: DW_MAHARAM_BUDGET.
+ * Cap-aware: draws from its OWN finite 'maharam' budget category (~100/day, auto-zeroes on
+ *   completion — TK-00060 DTD-A). NOT 'upload' (would starve new arrivals) and no longer 'backlog'
+ *   (the 5-field drain consumed it before the 05:30 drip ran → 0 grants). Override: DW_MAHARAM_BUDGET.
  */
 import fs from 'node:fs';
 import path from 'node:path';
@@ -24,7 +25,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_MAHARAM_BUDGET || 'backlog';
+const BUDGET_CAT = process.env.DW_MAHARAM_BUDGET || 'maharam';   // TK-00060 DTD-A: own finite category (was 'backlog' — got starved by the 5-field drain)
 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/maharam-onboard/maharam-daily-drip.sh b/scripts/maharam-onboard/maharam-daily-drip.sh
index 99df316..90dcba2 100755
--- a/scripts/maharam-onboard/maharam-daily-drip.sh
+++ b/scripts/maharam-onboard/maharam-daily-drip.sh
@@ -4,15 +4,17 @@
 # custom+dwc mfr metafields, global.width, ≥1 image, PG-FIRST shopify_product_id write-back), then
 # go-live those drafts (activate + inventory 2026 + 12 channels, Google excluded) so they auto-join
 # the maharam-wallcoverings smart collection. Resumable + idempotent (skips live/created SKUs).
-# Metered by the shared 'backlog' variant budget (budget.cjs) — never bursts, cooperates with the
-# 5-field backlog drain. Held OUT of the shared hourly cadence via vendors.js mahDrip:true.
+# Metered by its OWN finite 'maharam' variant-budget category (budget.cjs, ~100/day, auto-zeroes on
+# completion — TK-00060 DTD-A). Never bursts; can't be starved by the 5-field 'backlog' drain (the bug
+# that granted 0/day) nor breach Shopify's ~1k/day cap. Held OUT of the shared hourly cadence via
+# vendors.js mahDrip:true.
 set -uo pipefail
 # launchd runs with a minimal PATH — node (/usr/local/bin) + psql (homebrew) aren't on it.
 export PATH="/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:$PATH"
 DIR="$HOME/Projects/designerwallcoverings/scripts/maharam-onboard"
 LOG="/tmp/dw-maharam-daily.log"
 KILL="$HOME/.dw-fixer-stop"
-DAILY_CAP="${MAHARAM_DAILY_CAP:-100}"     # ceiling; the 'backlog' budget caps the real number lower
+DAILY_CAP="${MAHARAM_DAILY_CAP:-100}"     # ceiling; the finite 'maharam' budget category caps the real number lower
 exec >>"$LOG" 2>&1
 echo "═══════ maharam-daily-drip $(date '+%F %T')  cap=$DAILY_CAP ═══════"
 [ -f "$KILL" ] && { echo "kill-switch $KILL present — skip"; exit 0; }
diff --git a/scripts/variant-budget/budget.cjs b/scripts/variant-budget/budget.cjs
index 58f6308..0de1ca9 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']);
+const VALID = new Set(['roll', 'sample', 'upload', 'osborne', 'backlog', 'maharam']);
 // '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
@@ -94,6 +94,15 @@ const reclaimActive = () =>
 // non-ledger cap-eaters at 5:30+) the 10/day is a true floor. Defaults to the cap.
 const OSBORNE_FLOOR = parseInt(process.env.DW_OSBORNE_FLOOR || String(OSBORNE_CAP), 10);
 const OSB_OUT = path.join(REPO_ROOT, 'scripts', 'osborne-onboard', 'out');
+// 'maharam' = the finite Maharam quote-only onboard (1,429 sample-only DRAFTS = 1 variant each).
+// TK-00060, DTD 5/5 verdict A (2026-07-27): modeled on 'osborne' — its OWN category + a floor
+// reservation (below) that auto-zeroes when created.jsonl reaches the payload total, so the daily
+// ~100/day trickle can neither be starved by the 5-field 'backlog' drain (the bug it hit) NOR breach
+// the Shopify ~1,000/day cap (clamped to remainingGlobal like every category). 1,429 → ~14 days.
+// REVERSIBLE — the reservation self-zeroes on completion; force off via DW_MAHARAM_CAP=0.
+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');
 
 const today = () => {
   const d = new Date();
@@ -131,8 +140,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 }
-      : { roll: ROLL_CAP, sample: 0, upload: UPLOAD_CAP, osborne: OSBORNE_CAP, backlog: BACKLOG_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 },
   };
 }
 
@@ -151,6 +160,21 @@ function osborneReservation(spentOsborne) {
   return Math.min(Math.max(0, OSBORNE_FLOOR - spentOsborne), remainingProducts * 2);
 }
 
+// ── maharam hard-floor reservation (finite; auto-reverts on completion) ────────
+// Same shape as osborneReservation but 1 variant/product (sample-only). The unspent floor,
+// never more than the ACTUAL remaining maharam work. Auto-zeroes the day the 1,429-draft
+// onboard finishes → the reserved slice hands straight back to the pool (no calendar guess).
+function maharamReservation(spentMaharam) {
+  let remainingProducts = 0;
+  try {
+    const total = fs.readFileSync(path.join(MAH_OUT, 'payloads.jsonl'), 'utf8').split('\n').filter(Boolean).length;
+    let created = 0;
+    try { created = fs.readFileSync(path.join(MAH_OUT, 'created.jsonl'), 'utf8').split('\n').filter(Boolean).length; } catch (_) {}
+    remainingProducts = Math.max(0, total - created);
+  } catch (_) { return 0; }   // no maharam manifest → reserve nothing
+  return Math.min(Math.max(0, MAHARAM_FLOOR - spentMaharam), remainingProducts);
+}
+
 // ── mkdir-mutex (atomic create; stale-break after 10s) ─────────────────────────
 function withLock(fn) {
   fs.mkdirSync(LEDGER_DIR, { recursive: true });
@@ -174,10 +198,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;
       return j;
     }
   } catch (e) {}
-  return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0 } };
+  return { date: today(), spent: { roll: 0, sample: 0, upload: 0, osborne: 0, backlog: 0, maharam: 0 } };
 }
 function save(j) {
   const tmp = `${ledgerFile()}.tmp-${process.pid}`;
@@ -190,19 +215,23 @@ function save(j) {
  * `who` AND the global daily ceiling. Records the grant. Fail-open on any error.
  */
 function take(who, n) {
-  if (!VALID.has(who)) throw new Error(`budget.take: who must be roll|sample|upload|osborne, got ${who}`);
+  if (!VALID.has(who)) throw new Error(`budget.take: who must be ${[...VALID].join('|')}, got ${who}`);
   n = Math.max(0, parseInt(n, 10) || 0);
   if (!n) return 0;
   try {
     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;
+      const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam;
       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
       // against the full remaining (it owns the reservation).
       if (who !== 'osborne') remainingGlobal = Math.max(0, remainingGlobal - osborneReservation(j.spent.osborne));
+      // Same finite-floor protection for maharam: its unspent reserved slice is subtracted from
+      // 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));
       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;
@@ -228,7 +257,7 @@ function take(who, n) {
  * already-recorded debit in place) rather than fabricating headroom.
  */
 function refund(who, n) {
-  if (!VALID.has(who)) throw new Error(`budget.refund: who must be roll|sample|upload|osborne, got ${who}`);
+  if (!VALID.has(who)) throw new Error(`budget.refund: who must be ${[...VALID].join('|')}, got ${who}`);
   n = Math.max(0, parseInt(n, 10) || 0);
   if (!n) return 0;
   try {
@@ -251,14 +280,15 @@ function refund(who, n) {
  * returns Infinity so a ledger fault never freezes a live pipeline.
  */
 function remaining(who) {
-  if (!VALID.has(who)) throw new Error(`budget.remaining: who must be roll|sample|upload|osborne, got ${who}`);
+  if (!VALID.has(who)) throw new Error(`budget.remaining: who must be ${[...VALID].join('|')}, got ${who}`);
   try {
     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;
+      const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam;
       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 === 'backlog' && reclaimActive()) return globalLeft;
       return Math.min(Math.max(0, ph.caps[who] - j.spent[who]), globalLeft);
     });
@@ -268,15 +298,17 @@ 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;
+  const spentTotal = j.spent.roll + j.spent.sample + j.spent.upload + j.spent.osborne + j.spent.backlog + j.spent.maharam;
   const globalLeft = Math.max(0, BUDGET_TOTAL - spentTotal);
   const osborneReserved = osborneReservation(j.spent.osborne);   // unspent hard floor (auto-zeroes on completion)
-  const othersGlobal = Math.max(0, globalLeft - osborneReserved); // what roll/sample/upload may draw
+  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
   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,
     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),
@@ -285,6 +317,7 @@ function status() {
       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), globalLeft),
+      maharam: Math.min(Math.max(0, ph.caps.maharam - j.spent.maharam), Math.max(0, globalLeft - osborneReserved)),
       global: globalLeft },
   };
 }

← f951539 auto-save: 2026-07-27T13:51:03 (3 files) — scripts/sample-sp  ·  back to Designerwallcoverings  ·  TK-00060: contrarian FIX-FIRST fixes on maharam budget (roun 28920ac →