[object Object]

← back to Designer Wallcoverings

Wire Phillip Jeffries into cadence as gated 10/day DRAFT-only drip

289f3d9ad1d96ea91a67eb036ebafe6c7d89910e · 2026-06-30 16:40:43 -0700 · Steve Abrams

PJ-scoped, additive: vendors.js entry (sampleOnly + new draftOnly/dailyCap/
imageOverride/pjDrip flags) + importer honors them. draftOnly hard-forces
status=DRAFT even with --activate (PJ never auto-activates unpriced); dailyCap:10
counted from the dated restore ledger; imageOverride forces the PJ logo; pjDrip
gates PJ behind PJ_DRAFT_DRIP (independent of the Quadrille drip). PJ lifted from
DENY_VENDORS. OFF by default (inert until PJ_DRAFT_DRIP=1). Dedup pre-enforced in
pj_draft_catalog: 1,956 truly-net-new vs ALL DWJP + vendor=Phillip Jeffries across
every status + pj_catalog. Dry-run verified: 10 net-new, status=DRAFT, logo image,
0 would activate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 289f3d9ad1d96ea91a67eb036ebafe6c7d89910e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 30 16:40:43 2026 -0700

    Wire Phillip Jeffries into cadence as gated 10/day DRAFT-only drip
    
    PJ-scoped, additive: vendors.js entry (sampleOnly + new draftOnly/dailyCap/
    imageOverride/pjDrip flags) + importer honors them. draftOnly hard-forces
    status=DRAFT even with --activate (PJ never auto-activates unpriced); dailyCap:10
    counted from the dated restore ledger; imageOverride forces the PJ logo; pjDrip
    gates PJ behind PJ_DRAFT_DRIP (independent of the Quadrille drip). PJ lifted from
    DENY_VENDORS. OFF by default (inert until PJ_DRAFT_DRIP=1). Dedup pre-enforced in
    pj_draft_catalog: 1,956 truly-net-new vs ALL DWJP + vendor=Phillip Jeffries across
    every status + pj_catalog. Dry-run verified: 10 net-new, status=DRAFT, logo image,
    0 would activate.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 shopify/scripts/cadence/cadence-import.js | 82 ++++++++++++++++++++++++++-----
 shopify/scripts/cadence/vendors.js        | 19 +++++++
 2 files changed, 88 insertions(+), 13 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index c39cd0e6..73578fe5 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -51,6 +51,12 @@ const ADD_TO_COLLECTIONS = flag('--collections');       // GATED: add newly-publ
 // runner won't create any sample-only product until the env/flag is enabled. The few-SKU manual
 // test bypasses the gate via --sampleonly (or --only <SampleOnlyVendor>, which whitelists it).
 const SAMPLEONLY_DRIP = flag('--sampleonly') || process.env.QUADRILLE_SAMPLEONLY_DRIP === '1';
+// PJ-ONLY drip gate (Steve 2026-06-30, "draft drip held unpriced"). The Phillip Jeffries entry
+// (cfg.pjDrip) is its OWN sample-only family, kept SEPARATE from the held Quadrille drip above so
+// enabling PJ does not switch on Quadrille (and vice-versa). OFF by default — committing this code
+// is INERT: the scheduled hourly runner won't create any PJ product until PJ_DRAFT_DRIP=1 (or
+// --pj-drip) is set. A manual --only "Phillip Jeffries" run also whitelists it for the dry-run/test.
+const PJ_DRIP = flag('--pj-drip') || process.env.PJ_DRAFT_DRIP === '1';
 // DWBR house-line remap (Steve 2-brand rule, 2026-06-25). OFF by default — when OFF, vendors that
 // declare a houseLineCol (today only 'Brewster & York') keep emitting the map key as the product
 // vendor/Brand and are HELD by bannedBrandReason() (nothing publishes), so committing the remap
@@ -77,7 +83,11 @@ const TODAY = new Date().toISOString().slice(0, 10);
 // Defensive vendor denylist — even if a vendor is in vendors.js + discount_confirmed, NEVER
 // import these (belt-and-suspenders for sub-brand/registry drift). Today vendors.js already
 // excludes them by omission; this hard-blocks a future accidental add. (compliance officer, 2026-06-17)
-const DENY_VENDORS = new Set(['Nicolette Mayer','Phillip Jeffries','Et Cie','Spoonflower','Colefax & Fowler','Carlisle','Paul Montgomery','Cowtan & Tout','Rebel Walls','Koroseal','Newmor','Wolf Gordon','Scalamandre','Desima',
+// Phillip Jeffries LIFTED from the denylist 2026-06-30 (Steve approved the DRAFT-only drip):
+// it now imports via the gated, DRAFT-only, dailyCap:10 pjDrip path (vendors.js). Dedup is
+// pre-enforced in pj_draft_catalog (1,956 truly-net-new vs ALL DWJP + vendor='Phillip Jeffries'
+// rows across every status + pj_catalog), so the never-duplicate rule holds. NEVER --activate PJ.
+const DENY_VENDORS = new Set(['Nicolette Mayer','Et Cie','Spoonflower','Colefax & Fowler','Carlisle','Paul Montgomery','Cowtan & Tout','Rebel Walls','Koroseal','Newmor','Wolf Gordon','Scalamandre','Desima',
   // Romo LIFTED 2026-06-23 (Steve: "WE WANT ROMO") — tariff-treatment hold cleared; cost = romo_catalog landed (trade+tariff) per vendors.js.
   // Anna French LIFTED 2026-06-25 (officer vp-dw-commerce sign-off): the 254-duplicate hazard is
   // NEUTRALIZED — 578 live staging rows were link-and-skipped (anna_french_catalog.shopify_product_id
@@ -308,12 +318,16 @@ function gate() {
   for (const [vendor, cfg] of Object.entries(VENDORS)) {
     if (DENY_VENDORS.has(vendor)) { rows.push({ vendor, cfg, ready: false, why: 'DENYLISTED', netnew: 0 }); continue; }
     if (cfg.sampleOnly) {
-      // The full sample-only drip is HELD OFF until Steve enables it (QUADRILLE_SAMPLEONLY_DRIP=1
-      // or --sampleonly). Exception: an explicit --only <thisVendor> whitelists it for the manual
-      // few-SKU test. Otherwise report gated so the scheduled rotation never picks it up.
+      // The sample-only drip is HELD OFF until Steve enables it. PJ (cfg.pjDrip) has its OWN gate
+      // (PJ_DRAFT_DRIP / --pj-drip) so it stays independent of the Quadrille drip
+      // (QUADRILLE_SAMPLEONLY_DRIP / --sampleonly). Exception: an explicit --only <thisVendor>
+      // whitelists it for the manual few-SKU test. Otherwise report gated so the scheduled
+      // rotation never picks it up.
       const explicitlyTargeted = ONLY && ONLY.toLowerCase() === vendor.toLowerCase();
-      if (!SAMPLEONLY_DRIP && !explicitlyTargeted) {
-        rows.push({ vendor, cfg, ready: false, why: 'sampleonly-drip-gated (set QUADRILLE_SAMPLEONLY_DRIP=1 to enable)', netnew: 0 });
+      const dripOn = cfg.pjDrip ? PJ_DRIP : SAMPLEONLY_DRIP;     // PJ-scoped vs Quadrille-scoped enable
+      const dripEnv = cfg.pjDrip ? 'PJ_DRAFT_DRIP=1' : 'QUADRILLE_SAMPLEONLY_DRIP=1';
+      if (!dripOn && !explicitlyTargeted) {
+        rows.push({ vendor, cfg, ready: false, why: `sampleonly-drip-gated (set ${dripEnv} to enable)`, netnew: 0 });
         continue;
       }
       // no cost, no discount gate — just net-new rows for this brand
@@ -488,7 +502,9 @@ function buildSampleOnlyInput(vendor, cfg, row, activate) {
     variants: [{ sku: `${row.dw_sku}-Sample` }],
   });
   const ready = gate.ok;
-  const willActivate = !!(activate && ready);
+  // draftOnly (PJ, Steve 2026-06-30): HARD-force DRAFT even when --activate is passed, so an
+  // unpriced PJ product can NEVER auto-activate. PJ activation waits on confirmed wholesale cost.
+  const willActivate = cfg.draftOnly ? false : !!(activate && ready);
 
   // Tags: vendor + productType give the required ≥2; add collection, color, AI-derived tags, and a
   // Sample-Only marker. de-duped. gate.tags carry any Needs-* flags for DRAFT products.
@@ -512,11 +528,16 @@ function buildSampleOnlyInput(vendor, cfg, row, activate) {
     ],
   };
   // Attach ALL vendor images (full-page-scrape rule), primary first, de-duped.
+  // imageOverride (PJ, Steve 2026-06-30): force a single image (the PJ LOGO) onto every Shopify
+  // product — never the real vendor photo. Real PJ photos live ONLY on the microsite. When set,
+  // it REPLACES the image list entirely (logo-only), matching the 2,473 live PJ products.
   const { toImageList } = require('../lib/validate-before-activate.js');
-  const imgUrls = [...new Set([
-    ...(row.image_url ? [row.image_url] : []),
-    ...toImageList(row.all_images),
-  ].map(u => String(u||'').trim()).filter(u => /^https?:\/\//i.test(u)))];
+  const imgUrls = cfg.imageOverride
+    ? [String(cfg.imageOverride).trim()].filter(u => /^https?:\/\//i.test(u))
+    : [...new Set([
+        ...(row.image_url ? [row.image_url] : []),
+        ...toImageList(row.all_images),
+      ].map(u => String(u||'').trim()).filter(u => /^https?:\/\//i.test(u)))];
   if (imgUrls.length) input.files = imgUrls.map(u => ({ originalSource: webImage(u), contentType:'IMAGE', alt: core }));
   return { input, title, retail: 0, willActivate, ready, sampleOnly: true };
 }
@@ -736,6 +757,24 @@ function appendRestore(rec) {
   catch (e) { /* never let audit logging break a create */ }
 }
 
+// ---- per-vendor dailyCap (Steve 2026-06-30, PJ 10/day) ----
+// Count how many products the cadence has CREATED today from a given staging table, by reading the
+// dated restore ledger (data/upload-restore-{TODAY}.jsonl — one new file per calendar day, shared
+// across all 24 hourly ticks). Only action:'created' counts (linked-existing dedup hits don't).
+// Used to stop a capped vendor (PJ) once it reaches cfg.dailyCap products in a day. Fail-open: a
+// missing/unreadable ledger returns 0 (the cap simply doesn't trip yet) — never blocks the run.
+function createdTodayForTable(table) {
+  try {
+    if (!fs.existsSync(RESTORE_FILE)) return 0;
+    let n = 0;
+    for (const line of fs.readFileSync(RESTORE_FILE, 'utf8').split('\n')) {
+      if (!line.trim()) continue;
+      try { const r = JSON.parse(line); if (r.table === table && r.action === 'created') n++; } catch {}
+    }
+    return n;
+  } catch { return 0; }
+}
+
 // ---- inventory=2026 on BOTH variants at activation (hard rule new-products-inventory-2026) ----
 // Reuses inventory-set-2026.js's exact mechanism: look up inventoryItem ids by SKU, then
 // inventorySetQuantities on_hand=2026 at the Ventura Blvd location. Inventory sets do NOT
@@ -904,7 +943,21 @@ if (require.main === module) (async () => {
   for (const r of picked) {
     if (hitMax || hitCap) break;
     if (COMMIT) ensureOurPriceCol(r.cfg.table);
-    const skus = selectSkus(r.cfg, SKUS_PER_VENDOR);
+    // dailyCap (PJ, Steve 2026-06-30): cap how many products this vendor may create per calendar day.
+    // remaining = cap − already-created-today (from the dated restore ledger). Clamp the select limit
+    // to the remaining allowance; if 0 left, skip the vendor this slot. PJ-scoped — vendors without
+    // cfg.dailyCap are unaffected (limit stays SKUS_PER_VENDOR). The cap is enforced again per-create
+    // below so a mid-batch boundary is respected even within a single select.
+    let limit = SKUS_PER_VENDOR, capRemaining = Infinity;
+    if (r.cfg.dailyCap != null) {
+      const already = createdTodayForTable(r.cfg.table);
+      capRemaining = Math.max(0, r.cfg.dailyCap - already);
+      limit = Math.min(SKUS_PER_VENDOR, capRemaining);
+      if (capRemaining <= 0) { console.log(`\n${r.vendor} — dailyCap ${r.cfg.dailyCap} reached today (${already} created) → skipping this slot.`); continue; }
+      console.log(`\n${r.vendor} — dailyCap ${r.cfg.dailyCap}: ${already} created today, ${capRemaining} left → selecting up to ${limit}.`);
+    }
+    const skus = selectSkus(r.cfg, limit);
+    let vendorCreated = 0;   // products this vendor created THIS slot (for the per-create dailyCap stop)
     console.log(`\n${r.vendor} — ${skus.length} SKU(s) (${r.cfg.sampleOnly ? 'SAMPLE-ONLY $'+SAMPLE_PRICE+', no cost' : 'cost '+r.cfg.costExpr}):`);
     for (const row of skus) {
       // sample-only (Quadrille-house): no roll, single $4.25 sample → retail is N/A (0).
@@ -929,9 +982,12 @@ if (require.main === module) (async () => {
       const res = await createProduct(r.cfg.table, row, payload);
       if (res.held) { held++; /* HELD already logged inside createProduct; counted, never published */ }
       else if (res.ok && res.action === 'linked-existing') { linked++; console.log(`  ↪ ${row.dw_sku} already on Shopify (${res.status}) → backfilled link ${String(res.pid).replace(/.*\//,'')}, skipped create (dedup guard)`); }
-      else if (res.ok) { created++; if (res.activated) activatedCount++; if (res.activated && res.skus) activatedSkus.push(...res.skus); if (res.published && res.pid) publishedProductIds.push(res.pid); if(created%10===0) process.stdout.write(`\r  created ${created}…`); }
+      else if (res.ok) { created++; vendorCreated++; if (res.activated) activatedCount++; if (res.activated && res.skus) activatedSkus.push(...res.skus); if (res.published && res.pid) publishedProductIds.push(res.pid); if(created%10===0) process.stdout.write(`\r  created ${created}…`); }
       else { failed++; console.log(`  ✗ ${row.dw_sku} ${JSON.stringify(res.err).slice(0,140)}`); }
       if (res.stop) { console.log(`\n⛔ DAILY_VARIANT_LIMIT reached — stopping (resets in ~24h). created ${created} this run.`); hitCap=true; break; }
+      // dailyCap per-create stop (PJ): once this vendor created its remaining daily allowance THIS
+      // slot, stop creating more of it (other vendors in `picked` continue). Counts only real creates.
+      if (r.cfg.dailyCap != null && vendorCreated >= capRemaining) { console.log(`\n· ${r.vendor} dailyCap ${r.cfg.dailyCap} reached (${vendorCreated} this slot) — stopping this vendor.`); break; }
       if (MAX_PRODUCTS && (created + linked) >= MAX_PRODUCTS) { console.log(`\n· --max ${MAX_PRODUCTS} reached — stopping this slot (resumes next slot via cursor).`); hitMax=true; break; }
     }
   }
diff --git a/shopify/scripts/cadence/vendors.js b/shopify/scripts/cadence/vendors.js
index a29bc930..aca97173 100644
--- a/shopify/scripts/cadence/vendors.js
+++ b/shopify/scripts/cadence/vendors.js
@@ -166,4 +166,23 @@ module.exports = {
   //       NEVER be customer-facing (overrides the general always-write-manufacturer_sku rule),
   //   (4) ~34 rows lack cost and will be skipped at the gate until cost is sourced.
   'Fentucci':   { table: 'fentucci_catalog',         costExpr: 'NULLIF(cost,0)', sellExpr: 'NULLIF(price,0)', soldBy: 'Yard', productType: 'Wallcovering', discountNote: 'INERT prep 2026-06-24 — see block above; do NOT enable without Steve.' },
+
+  // ── Phillip Jeffries — 10/day DRAFT-only drip, held UNPRICED (Steve approved 2026-06-30) ──────
+  // Onboard the net-new PJ gap (pj_draft_catalog, 1,956 truly-net-new rows, DWJP-14511+) as a SLOW
+  // DRAFT drip: logo image + pattern/color only, NO cost, held DRAFT until PJ wholesale cost lands.
+  // Uses the sampleOnly no-cost path BUT adds two PJ-only flags the importer honors:
+  //   draftOnly: true  → HARD-forces status='DRAFT' even if --activate is passed. PJ can NEVER
+  //                      auto-activate unpriced. (buildSampleOnlyInput willActivate is force-false.)
+  //   dailyCap: 10     → the cadence creates at most 10 PJ products per calendar day (counted from
+  //                      the cadence restore-ledger). Rate-limits the onboard to ~196 days.
+  //   imageOverride    → force the PJ LOGO on every Shopify product (matches the 2,473 live PJ that
+  //                      already show the logo). Real photos live ONLY on the microsite, never here.
+  //   pjDrip: true     → PJ-SPECIFIC enable marker. The importer gates this entry behind the PJ_DRAFT_DRIP
+  //                      env (or --pj-drip) so turning PJ on does NOT switch on the held Quadrille
+  //                      sample-only drip (QUADRILLE_SAMPLEONLY_DRIP), and vice-versa.
+  // NEVER pass --activate for PJ; logo-only on Shopify; dedup already done in pj_draft_catalog build.
+  'Phillip Jeffries': { table: 'pj_draft_catalog', sampleOnly: true, draftOnly: true, dailyCap: 10, pjDrip: true,
+                        imageOverride: 'https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_3266367c-1ae8-48d5-8b02-d033802bee77.png?v=1722462657',
+                        soldBy: 'Sample', productType: 'Wallcovering',
+                        note: 'DRAFT-only drip, held unpriced until PJ cost confirmed (Steve 2026-06-30). NEVER pass --activate for PJ. Logo image only; dedup-clean net-new in pj_draft_catalog (DWJP-14511+).' },
 };

← 859470c8 auto-save: 2026-06-30T15:41:34 (6 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-30T16:41:53 (8 files) — pending-approval/ 11b2896f →