From f2230c93869a226ae0ac2d60b48e25ace2d079aa Mon Sep 17 00:00:00 2001 From: Luc Date: Sat, 18 Apr 2026 12:22:14 +0200 Subject: [PATCH 1/2] fix(engine/v2): symmetrische archetype mapping en gebalanceerde occasion bias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QUIZ_STYLE_TO_ARCHETYPE: - classic: 1.0 CLASSIC (was 0.7/0.2/0.1 verdeeld over CLASSIC/BUSINESS/MINIMALIST) - smart-casual: 0.7 SC + 0.3 CLASSIC (was pure 1.0 SC) OCCASION_ARCHETYPE_BIAS: - work: BUSINESS 0.25→0.15, CLASSIC 0.2→0.25 (minder aggressieve business-push) - casual: + AVANT_GARDE 0.1, + STREETWEAR 0.1 (meer variatie in casual) - date: + AVANT_GARDE 0.1 (ruimte voor expressievere date-looks) Co-Authored-By: Claude Opus 4.7 --- src/engine/v2/buildProfile.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/v2/buildProfile.ts b/src/engine/v2/buildProfile.ts index 811dbd0bd..8187dad62 100644 --- a/src/engine/v2/buildProfile.ts +++ b/src/engine/v2/buildProfile.ts @@ -16,10 +16,10 @@ import type { const QUIZ_STYLE_TO_ARCHETYPE: Record = { minimalist: { MINIMALIST: 1.0 }, - classic: { CLASSIC: 0.7, BUSINESS: 0.2, MINIMALIST: 0.1 }, + classic: { CLASSIC: 1.0 }, streetwear: { STREETWEAR: 1.0 }, - 'smart-casual': { SMART_CASUAL: 1.0 }, - smart_casual: { SMART_CASUAL: 1.0 }, + 'smart-casual': { SMART_CASUAL: 0.7, CLASSIC: 0.3 }, + smart_casual: { SMART_CASUAL: 0.7, CLASSIC: 0.3 }, athletic: { ATHLETIC: 1.0 }, sporty: { ATHLETIC: 1.0 }, rugged: { SMART_CASUAL: 0.5, STREETWEAR: 0.5 }, @@ -87,10 +87,10 @@ const DEFAULT_ARCHETYPES: ArchetypeWeights = { }; const OCCASION_ARCHETYPE_BIAS: Record = { - work: { BUSINESS: 0.25, CLASSIC: 0.2, SMART_CASUAL: 0.15, MINIMALIST: 0.1 }, + work: { BUSINESS: 0.15, CLASSIC: 0.25, SMART_CASUAL: 0.15, MINIMALIST: 0.1 }, formal: { BUSINESS: 0.4, CLASSIC: 0.2, MINIMALIST: 0.05 }, - casual: { SMART_CASUAL: 0.2, CLASSIC: 0.1 }, - date: { CLASSIC: 0.15, SMART_CASUAL: 0.15, MINIMALIST: 0.1 }, + casual: { SMART_CASUAL: 0.2, CLASSIC: 0.1, AVANT_GARDE: 0.1, STREETWEAR: 0.1 }, + date: { CLASSIC: 0.15, SMART_CASUAL: 0.15, MINIMALIST: 0.1, AVANT_GARDE: 0.1 }, travel: { SMART_CASUAL: 0.2, MINIMALIST: 0.15 }, sport: { ATHLETIC: 0.5 }, }; From bc7d9a8169073a1d69fdce2509df1c498f773e4e Mon Sep 17 00:00:00 2001 From: Luc Date: Sat, 18 Apr 2026 12:27:37 +0200 Subject: [PATCH 2/2] fix(engine/v2): relax formality-spread penalty for SMART_CASUAL and AVANT_GARDE The coherence checker penalized the signature looks of these archetypes: blazer+tee (smart-casual kern) triggered formality_mismatch, and avant-garde combos lost points for the proportional tension that defines the style. - evaluateCoherence now accepts an optional UserStyleProfile and picks archetype-aware formality thresholds. - SMART_CASUAL tolerates spread up to 0.55 without penalty (was ~0.25). - AVANT_GARDE tolerates spread up to 0.70 without penalty. - Default thresholds unchanged for other archetypes. - composer.scoreComposition forwards the profile to evaluateCoherence. Co-Authored-By: Claude Opus 4.7 --- src/engine/v2/coherence.ts | 55 +++++++++++++++++++++++++++++++++----- src/engine/v2/composer.ts | 7 ++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/engine/v2/coherence.ts b/src/engine/v2/coherence.ts index 753f0d563..a97e652d9 100644 --- a/src/engine/v2/coherence.ts +++ b/src/engine/v2/coherence.ts @@ -11,7 +11,49 @@ export interface CoherenceScores { reasons: string[]; } -export function evaluateCoherence(products: ScoredProduct[]): CoherenceScores { +interface FormalityThresholds { + hardPenalty: number; + midPenalty: number; + softPenalty: number; + mismatchTag: number; + coherentTag: number; +} + +function formalityThresholdsFor( + profile?: UserStyleProfile +): FormalityThresholds { + const primary = profile?.primaryArchetype; + if (primary === 'AVANT_GARDE') { + return { + hardPenalty: 0.85, + midPenalty: 0.8, + softPenalty: 0.7, + mismatchTag: 0.7, + coherentTag: 0.35, + }; + } + if (primary === 'SMART_CASUAL') { + return { + hardPenalty: 0.7, + midPenalty: 0.65, + softPenalty: 0.55, + mismatchTag: 0.55, + coherentTag: 0.3, + }; + } + return { + hardPenalty: 0.55, + midPenalty: 0.4, + softPenalty: 0.25, + mismatchTag: 0.45, + coherentTag: 0.25, + }; +} + +export function evaluateCoherence( + products: ScoredProduct[], + profile?: UserStyleProfile +): CoherenceScores { const reasons: string[] = []; if (products.length === 0) { return { @@ -33,16 +75,17 @@ export function evaluateCoherence(products: ScoredProduct[]): CoherenceScores { if (colorHarmony > 0.8) reasons.push('color_harmony_strong'); else if (colorHarmony < 0.45) reasons.push('color_harmony_weak'); + const thresholds = formalityThresholdsFor(profile); const formalities = products.map((p) => p.formality); const minF = Math.min(...formalities); const maxF = Math.max(...formalities); const spread = maxF - minF; let formalitySpread = 1; - if (spread > 0.55) formalitySpread = 0.35; - else if (spread > 0.4) formalitySpread = 0.6; - else if (spread > 0.25) formalitySpread = 0.85; - if (spread < 0.25) reasons.push('formality_coherent'); - if (spread > 0.45) reasons.push('formality_mismatch'); + if (spread > thresholds.hardPenalty) formalitySpread = 0.35; + else if (spread > thresholds.midPenalty) formalitySpread = 0.6; + else if (spread > thresholds.softPenalty) formalitySpread = 0.85; + if (spread < thresholds.coherentTag) reasons.push('formality_coherent'); + if (spread > thresholds.mismatchTag) reasons.push('formality_mismatch'); const archetypeTotals: Record = {}; for (const p of products) { diff --git a/src/engine/v2/composer.ts b/src/engine/v2/composer.ts index 340377203..db07c1ad5 100644 --- a/src/engine/v2/composer.ts +++ b/src/engine/v2/composer.ts @@ -132,9 +132,10 @@ function assembleReasons( } function scoreComposition( - products: ScoredProduct[] + products: ScoredProduct[], + profile: UserStyleProfile ): { coherence: ReturnType; score: number } { - const coherence = evaluateCoherence(products); + const coherence = evaluateCoherence(products, profile); const productAvg = products.reduce((acc, p) => acc + p.score, 0) / Math.max(1, products.length); const baseScore = productAvg * 0.55 + coherence.combined * 0.45; @@ -274,7 +275,7 @@ function composeForOccasion( if (seenSignatures.has(signature)) continue; seenSignatures.add(signature); - const { coherence, score } = scoreComposition(products); + const { coherence, score } = scoreComposition(products, profile); if (score < 0.35) continue; candidates.push({