← 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
M shopify/scripts/cadence/cadence-import.jsM shopify/scripts/cadence/vendors.js
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 →