← back to Dw Daily Catchup 20260909
collapse rotation-order.js to single source: symlink activator copy → canonical calendar copy (kills the two-drifting-copies footgun)
382423fa104f58a6fa0ec7148d791b65d4486610 · 2026-08-11 13:39:51 -0700 · steve@designerwallcoverings.com
Files touched
Diff
commit 382423fa104f58a6fa0ec7148d791b65d4486610
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Tue Aug 11 13:39:51 2026 -0700
collapse rotation-order.js to single source: symlink activator copy → canonical calendar copy (kills the two-drifting-copies footgun)
---
lib/rotation-order.js | 118 +-------------------------------------------------
1 file changed, 1 insertion(+), 117 deletions(-)
diff --git a/lib/rotation-order.js b/lib/rotation-order.js
deleted file mode 100644
index 3446c58..0000000
--- a/lib/rotation-order.js
+++ /dev/null
@@ -1,117 +0,0 @@
-'use strict';
-/*
- * rotation-order.js — THE canonical ordering for DW draft activation.
- *
- * ONE source of truth for the "textures-first, then vendor round-robin (~one per
- * vendor per increment)" order across ALL DRAFT products (not just the ~5k
- * field-fix worklist). Both consumers import ROTATION_ORDER_SQL from here so the
- * unified effective order is identical:
- * - dw-rotation-activator/rotate-activate.js (the executor that flips DRAFT→ACTIVE)
- * - dw-activation-calendar/scripts/generate-schedule.js (the projection/UI)
- *
- * Proven design (read-only projection validated by the main session):
- * mat_tier = 0 (textures/naturals) leads, then mat_tier = 1 (rest).
- * Within each tier, breadth-first vendor round-robin:
- * rr = (position of this SKU within its (tier,vendor) group) * 100000
- * + (dense rank of the vendor within the tier)
- * Ordering by (mat_tier, rr) therefore takes ONE sku from every vendor before a
- * 2nd from any — so each activation increment spreads across vendors (never a
- * single-vendor contiguous block).
- *
- * WIDENED texture detection (vs the original material-metafield-only heuristic,
- * which under-counted because the material metafield is blank on many drafts):
- * we build a match string from title + product_type + tags + BOTH material
- * metafields, and match a broad naturals/texture lexicon. This lands more true
- * grasscloth / silk / naturals / weaves / metallics in tier 0.
- *
- * NOISE FILTER: instructional/help-doc rows (e.g. a "How to Hang Grasscloth" help
- * page that falsely matched the grasscloth keyword) are EXCLUDED entirely so they
- * never activate. The filter is deliberately NARROW — it matches genuine help-doc
- * title patterns only, NOT "memo"/"sample" (which are legit product-name tokens on
- * Phillipe Romano etc.).
- *
- * Schumacher is excluded (internal-only, HARD RULE — never on the storefront).
- */
-
-// Broad naturals/texture lexicon → tier 0 (textures-first).
-const TEXTURE_REGEX =
- "silk|grasscloth|sisal|raffia|abaca|jute|linen|hemp|cork|paperweave|paper weave|" +
- "grass|natural fiber|natural fibre|seagrass|arrowroot|wool|mohair|leather|suede|" +
- "bamboo|rattan|cane|weave|woven|textile|\\mfiber\\M|\\mfibre\\M|reed|coir|hessian|" +
- "burlap|felt|velvet|flock|metallic|\\mmica\\M|gilded|gold leaf|silver leaf|foil";
-
-// NARROW instructional/help-doc noise filter. Matches real help-doc titles only.
-// Deliberately does NOT include "memo"/"sample book" (legit product-name tokens).
-const NOISE_REGEX =
- "how to|how-to|instructional|installation guide|hanging guide|care guide|" +
- "user guide|tutorial";
-
-// NEVER-ACTIVATE vendors (2026-08-10, TK-10446). These are bespoke / made-to-order /
-// quote-only / internal-only lines that must never auto-publish to the storefront:
-// Gracie — HARD RULE, Steve "do not show GRACIE EVER" (bespoke chinoiserie, quote-only)
-// Fromental, Zuber, De Gournay — bespoke made-to-order, quote-only, settlement-heavy
-// Astek — internal line-viewer only, not on the DW Shopify store
-// (Schumacher is already excluded below as internal-only.)
-const NEVER_ACTIVATE_VENDORS = "gracie|fromental|zuber|de gournay|astek";
-
-// SAMPLE-SKU filter (2026-08-10, TK-10446). ~90% of the DRAFT pool are memo/sample
-// SKUs (…-Sample, …-S). Activating one publishes a standalone $4.25 memo listing
-// (Google-Merchant $4.25-leak / disapproval risk). Samples must NEVER activate as
-// their own live product — only real roll/sell SKUs do. Matched on the SKU suffix,
-// NOT on title (Phillipe Romano etc. use "memo"/"sample" as legit name tokens).
-const SAMPLE_SKU_REGEX = "-(sample|s)$";
-
-/*
- * The canonical ordered-DRAFT query. Emits every DRAFT (minus Schumacher + noise)
- * in the exact textures-first + vendor-round-robin order, with the columns both
- * consumers need. shopify_id is the durable per-item identity + image/title join.
- *
- * NOTE: `tags` is a text column (comma-joined), `metafields` is jsonb. coalesce
- * guards every field so a null never breaks the match string.
- */
-const ROTATION_ORDER_SQL = `
-WITH pool AS (
- SELECT
- shopify_id,
- vendor,
- coalesce(sku, dw_sku) AS dw_sku,
- dw_sku AS raw_dw_sku,
- title,
- product_type,
- lower(
- coalesce(metafields->'specs'->>'material','') || ' ' ||
- coalesce(metafields->'custom'->>'material','') || ' ' ||
- coalesce(title,'') || ' ' ||
- coalesce(tags,'') || ' ' ||
- coalesce(product_type,'')
- ) AS matstr
- FROM shopify_products sp
- WHERE status = 'DRAFT'
- AND vendor NOT ILIKE '%schumacher%' -- internal-only, never storefront
- AND lower(coalesce(vendor,'')) !~ '${NEVER_ACTIVATE_VENDORS}' -- Gracie/Fromental/Zuber/De Gournay/Astek: bespoke/quote-only/internal, never auto-publish
- AND lower(coalesce(title,'')) !~ '${NOISE_REGEX}' -- drop help-doc noise rows
- AND lower(coalesce(sku, dw_sku, '')) !~ '${SAMPLE_SKU_REGEX}' -- exclude memo/sample SKUs (…-Sample / …-S): never a standalone live listing
- AND NOT EXISTS ( -- re-introduction guard: skip a DRAFT whose title was EVER on the store
- SELECT 1 FROM shopify_products a -- (ACTIVE now, or intentionally ARCHIVED/DELETED before) — no resurrecting retired patterns
- WHERE a.status IN ('ACTIVE','ARCHIVED','DELETED_FROM_SHOPIFY')
- AND a.title = sp.title AND a.shopify_id <> sp.shopify_id
- )
-),
-tiered AS (
- SELECT *,
- CASE WHEN matstr ~ '${TEXTURE_REGEX}' THEN 0 ELSE 1 END AS mat_tier
- FROM pool
-),
-ranked AS (
- SELECT *,
- (row_number() OVER (PARTITION BY mat_tier, vendor
- ORDER BY coalesce(dw_sku, raw_dw_sku, shopify_id))) * 100000
- + dense_rank() OVER (PARTITION BY mat_tier ORDER BY vendor) AS rr
- FROM tiered
-)
-SELECT shopify_id, vendor, dw_sku, title, product_type, mat_tier, rr
-FROM ranked
-ORDER BY mat_tier, rr
-`;
-
-module.exports = { ROTATION_ORDER_SQL, TEXTURE_REGEX, NOISE_REGEX };
diff --git a/lib/rotation-order.js b/lib/rotation-order.js
new file mode 120000
index 0000000..a0362c1
--- /dev/null
+++ b/lib/rotation-order.js
@@ -0,0 +1 @@
+../../dw-activation-calendar/lib/rotation-order.js
\ No newline at end of file
← 81ca301 TK-10446 Tier-1: live-verify (892 orphans/48 have-roll/3 ina
·
back to Dw Daily Catchup 20260909
·
rotation drain: pre-flight re-intro guard check — abort acti 6074d75 →