← 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
M public/js/app.jsA scripts/test-tailor.js
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 →