[object Object]

← back to Dw Rotation Activator

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 Rotation Activator  ·  rotation drain: pre-flight re-intro guard check — abort acti 6074d75 →