[object Object]

← back to Whatsmystyle

test(tailor): 10 scoring assertions + bump era weight 1.0→1.5 so era-exact wins over saturated+formal stack

eb523a44f07bd494d68bb8ad4afa9bc8c437bc75 · 2026-05-12 08:02:56 -0700 · SteveStudio2

Files touched

Diff

commit eb523a44f07bd494d68bb8ad4afa9bc8c437bc75
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 08:02:56 2026 -0700

    test(tailor): 10 scoring assertions + bump era weight 1.0→1.5 so era-exact wins over saturated+formal stack
---
 public/js/app.js       |   7 ++-
 scripts/test-tailor.js | 118 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 123 insertions(+), 2 deletions(-)

diff --git a/public/js/app.js b/public/js/app.js
index 5ea596b..2c75f3d 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -552,11 +552,14 @@ const TONE_CASUAL = /\b(tee|t-shirt|hoodie|sweatpant|jean|denim|sneaker|track|jo
 
 function tailorScore(it, tailor) {
   let s = 0;
-  // Era — Gaussian-ish: full credit at exact match, drop to 0 at ±25 years
+  // Era — primary axis. Tested 2026-05-12: with weight 1.0, saturated+formal
+  // items outranked era-exact matches at high palette/tone settings. Era is
+  // *the* signal for "what show is this", so bump to 1.5 so era-exact wins
+  // even when palette+tone both hit (max combined = 0.8 + 0.7 = 1.5).
   const yr = eraOf(it);
   if (yr !== null) {
     const delta = Math.abs(yr - tailor.era);
-    s += Math.max(0, 1 - delta / 25) * 1.0;
+    s += Math.max(0, 1 - delta / 25) * 1.5;
   }
   const text = `${it.title || ''} ${it.brand || ''} ${it.color || ''} ${it.tags || ''}`;
   // Palette — high palette → saturated bonus, low palette → neutral bonus
diff --git a/scripts/test-tailor.js b/scripts/test-tailor.js
new file mode 100644
index 0000000..d8a4f18
--- /dev/null
+++ b/scripts/test-tailor.js
@@ -0,0 +1,118 @@
+/**
+ * Tailor-the-view scoring tests (tick 16).
+ *
+ * The scoring heuristics live in public/js/app.js (browser-side). This file
+ * reimplements them as a pure CJS module so we can sanity-check the ranking
+ * without spinning up a headless browser. If the live UI is wrong, this test
+ * is no better than guessing — but if the test is wrong, the live UI is too,
+ * which is what we want to catch.
+ *
+ * Run:
+ *   node scripts/test-tailor.js
+ */
+const ERA_WORDS = [
+  [/\b(victorian|edwardian)\b/i, 1900],
+  [/\b(1920s|art deco|gatsby|jazz age)\b/i, 1925],
+  [/\b(1930s|depression|hollywood golden)\b/i, 1935],
+  [/\b(1940s|war[- ]era|forties)\b/i, 1945],
+  [/\b(1950s|fifties|midcentury|mid[- ]?century|atomic)\b/i, 1955],
+  [/\b(1960s|sixties|mod|psychedelic|space age)\b/i, 1965],
+  [/\b(1970s|seventies|disco|bohemian|hippie)\b/i, 1975],
+  [/\b(1980s|eighties|new wave|preppy revival|power)\b/i, 1985],
+  [/\b(1990s|nineties|grunge|minimal[- ]?ist)\b/i, 1995],
+  [/\b(y2k|early 2000s|2000s)\b/i, 2005],
+  [/\b(2010s)\b/i, 2015],
+  [/\b(modern|contemporary|current|today)\b/i, 2025],
+  [/\bvintage\b/i, 1965],
+  [/\bretro\b/i, 1975],
+];
+function eraOf(it) {
+  const text = `${it.title || ''} ${it.brand || ''} ${it.tags || ''}`;
+  for (const [re, year] of ERA_WORDS) if (re.test(text)) return year;
+  return null;
+}
+const PALETTE_SATURATED = /\b(red|crimson|cobalt|emerald|fuchsia|magenta|electric|neon|vibrant|saffron|chartreuse|tangerine|hot pink|royal blue)\b/i;
+const PALETTE_NEUTRAL   = /\b(beige|stone|ivory|cream|oat|sand|taupe|nude|mushroom|greige|bone|natural)\b/i;
+const TONE_FORMAL = /\b(blazer|tuxedo|gown|silk|wool|tweed|cashmere|tailored|suit|oxford|loafer|trouser|evening|cocktail)\b/i;
+const TONE_CASUAL = /\b(tee|t-shirt|hoodie|sweatpant|jean|denim|sneaker|track|jogger|cargo|baseball)\b/i;
+
+function tailorScore(it, tailor) {
+  let s = 0;
+  const yr = eraOf(it);
+  if (yr !== null) {
+    const delta = Math.abs(yr - tailor.era);
+    s += Math.max(0, 1 - delta / 25) * 1.5;
+  }
+  const text = `${it.title || ''} ${it.brand || ''} ${it.color || ''} ${it.tags || ''}`;
+  const sat = PALETTE_SATURATED.test(text) ? 1 : 0;
+  const neu = PALETTE_NEUTRAL.test(text) ? 1 : 0;
+  const palT = tailor.palette / 100;
+  s += sat * palT * 0.8 + neu * (1 - palT) * 0.8;
+  const fml = TONE_FORMAL.test(text) ? 1 : 0;
+  const csl = TONE_CASUAL.test(text) ? 1 : 0;
+  const tonT = tailor.tone / 100;
+  s += fml * tonT * 0.7 + csl * (1 - tonT) * 0.7;
+  return s;
+}
+
+const FIXTURES = [
+  { id: 1, title: 'Disco Wrap Dress',       brand: 'Halston',     color: 'gold',     tags: '1970s, bohemian' },
+  { id: 2, title: 'Midcentury Sheath',      brand: 'Dior',        color: 'ivory',    tags: 'fifties, atomic' },
+  { id: 3, title: 'Y2K Cargo Pants',        brand: 'Diesel',      color: 'denim',    tags: 'y2k, low rise' },
+  { id: 4, title: 'Modern Wool Blazer',     brand: 'Theory',      color: 'charcoal', tags: 'contemporary, tailored' },
+  { id: 5, title: 'Vibrant Cobalt Trouser', brand: 'Marni',       color: 'cobalt',   tags: 'modern' },
+  { id: 6, title: 'Stone Linen Tee',        brand: 'Everlane',    color: 'stone',    tags: 'casual basics' },
+  { id: 7, title: 'Cashmere Evening Gown',  brand: 'The Row',     color: 'black',    tags: 'formal' },
+  { id: 8, title: 'Vintage Hoodie',         brand: 'Champion',    color: 'grey',     tags: 'retro casual' },
+];
+
+let pass = 0, fail = 0;
+const failures = [];
+function expect(name, cond, detail) {
+  if (cond) { pass++; }
+  else { fail++; failures.push(`[${name}] ${detail}`); }
+}
+
+// Test 1 — era detection
+expect('era:halston',   eraOf(FIXTURES[0]) === 1975, `got ${eraOf(FIXTURES[0])}, want 1975`);
+expect('era:dior',      eraOf(FIXTURES[1]) === 1955, `got ${eraOf(FIXTURES[1])}, want 1955`);
+expect('era:y2k',       eraOf(FIXTURES[2]) === 2005, `got ${eraOf(FIXTURES[2])}, want 2005`);
+expect('era:theory',    eraOf(FIXTURES[3]) === 2025, `got ${eraOf(FIXTURES[3])}, want 2025`);
+expect('era:everlane',  eraOf(FIXTURES[5]) === null, `unknown era should be null, got ${eraOf(FIXTURES[5])}`);
+
+// Test 2 — Steve's default tailor (Era 1975, Palette 93, Density 277, Tone 73)
+// Expectations:
+//  - Disco Wrap Dress (Halston, 1975, gold/bohemian) ranks #1 — exact era hit
+//  - Vibrant Cobalt Trouser (modern, saturated, formal) ranks high — saturated + formal hits
+//  - Stone Linen Tee (neutral, casual) ranks LOW — neutral conflicts with palette=93, casual conflicts with tone=73
+const steveDefaults = { era: 1975, palette: 93, density: 277, tone: 73 };
+const scored = FIXTURES.map(f => ({ ...f, score: tailorScore(f, steveDefaults) }));
+scored.sort((a, b) => b.score - a.score);
+expect('rank:halston-first', scored[0].id === 1, `expected Halston Disco first, got ${scored[0].title}`);
+const tee = scored.find(s => s.id === 6);
+const cobalt = scored.find(s => s.id === 5);
+expect('rank:cobalt-beats-tee', cobalt.score > tee.score, `cobalt score ${cobalt.score.toFixed(3)} should beat tee ${tee.score.toFixed(3)}`);
+
+// Test 3 — palette inversion. Drop palette to 5 and stone-tee should now beat cobalt.
+const lowPalette = { era: 1975, palette: 5, density: 277, tone: 73 };
+const teeLow    = tailorScore(FIXTURES[5], lowPalette);
+const cobaltLow = tailorScore(FIXTURES[4], lowPalette);
+expect('palette-inversion', teeLow > cobaltLow, `at palette=5, tee ${teeLow.toFixed(3)} should beat cobalt ${cobaltLow.toFixed(3)}`);
+
+// Test 4 — tone inversion. Drop tone to 5 and hoodie should beat evening gown.
+const lowTone = { era: 2000, palette: 50, density: 277, tone: 5 };
+const hoodie  = tailorScore(FIXTURES[7], lowTone);
+const gown    = tailorScore(FIXTURES[6], lowTone);
+expect('tone-inversion', hoodie > gown, `at tone=5, hoodie ${hoodie.toFixed(3)} should beat gown ${gown.toFixed(3)}`);
+
+// Test 5 — era window. Item with 1975 hit should score zero at era=2025 (Δ=50 > 25 window).
+const farEra = tailorScore(FIXTURES[0], { era: 2025, palette: 0, density: 277, tone: 0 });
+expect('era-window-cutoff', farEra < 0.01, `Halston at era=2025 should score ~0, got ${farEra.toFixed(3)}`);
+
+console.log(JSON.stringify({
+  ok: fail === 0,
+  pass, fail,
+  failures: failures.slice(0, 20),
+  top3_at_steve_defaults: scored.slice(0, 3).map(s => ({ title: s.title, score: Number(s.score.toFixed(3)) })),
+}, null, 2));
+process.exit(fail === 0 ? 0 : 1);

← 73ee5d4 feat(tailor): set-decorator 'Tailor the view' panel — Era/Pa  ·  back to Whatsmystyle  ·  yolo tick 16: pro_grade catalog filter + production picker o f20ac43 →