Fix size-aware product suggestions in resolver

Previously a different-size variant was boosted to 0.95 confidence
('same product, different size'), so a Small order would be confidently
suggested the Large product. Now suggestions require the same size; a
known different size is suppressed entirely (you create the right one
instead). Also handle 'Size: X for ...' phrasing, medium sizes, and
American 'Color:' spelling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-06-13 13:05:17 +10:00
parent 3c3adcdea9
commit 46d1ca3375

View file

@ -260,6 +260,31 @@ const calculateTitleSimilarity = (title1: string, title2: string): number => {
return matchingWords / totalUniqueWords; return matchingWords / totalUniqueWords;
}; };
/**
* Extract a size token (large/med/small) from a title or variant string.
* Prefers an explicit "Size: X" so trailing words like "Large for med plants"
* don't get misread.
*/
export const extractSize = (text: string): string => {
const lower = text.toLowerCase();
const explicit = lower.match(/size:\s*(large|medium|med|small)/);
let token = explicit ? explicit[1] : '';
if (!token) {
if (lower.includes('large') && !lower.includes('small')) token = 'large';
else if (lower.includes('small') && !lower.includes('large')) token = 'small';
}
return token === 'medium' ? 'med' : token;
};
const cleanForCompare = (title: string): string =>
title
.replace(/Colou?r:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') // strip Color/Colour
.replace(/Size:\s*[^,\s-]+(?:\s+(?:for|plants?|succulents?)\b[^,]*)?/i, '') // strip "Size: X for ..."
.replace(/3D-Printed/i, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
/** /**
* Find potential matches for a missing product * Find potential matches for a missing product
* This helps suggest existing products that might be similar * This helps suggest existing products that might be similar
@ -270,77 +295,54 @@ export const findPotentialMatches = (
maxSuggestions: number = 5 maxSuggestions: number = 5
): Array<{product: any; confidence: number; reason: string}> => { ): Array<{product: any; confidence: number; reason: string}> => {
const suggestions: Array<{product: any; confidence: number; reason: string}> = []; const suggestions: Array<{product: any; confidence: number; reason: string}> = [];
const itemTitle = missingProductTitle.toLowerCase(); const itemSize = extractSize(missingProductTitle);
let itemSize = ''; const cleanItemTitle = cleanForCompare(missingProductTitle);
// Extract size from missing product
if (itemTitle.includes('large') && !itemTitle.includes('small')) {
itemSize = 'large';
} else if (itemTitle.includes('small') && !itemTitle.includes('large')) {
itemSize = 'small';
}
const cleanItemTitle = missingProductTitle
.replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '')
.replace(/Size:\s*[^,\s-]+/i, '')
.replace(/3D-Printed/i, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
for (const product of existingProducts) { for (const product of existingProducts) {
const productTitle = product.title.toLowerCase(); const productSize = extractSize(`${product.title} ${(product.aliases || []).join(' ')}`);
let productSize = ''; const cleanProductTitle = cleanForCompare(product.title);
if (productTitle.includes('large') && !productTitle.includes('small')) {
productSize = 'large';
} else if (productTitle.includes('small') && !productTitle.includes('large')) {
productSize = 'small';
}
const cleanProductTitle = product.title
.replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '')
.replace(/Size:\s*[^,\s-]+/i, '')
.replace(/3D-Printed/i, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle); const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle);
let reason = ''; let reason = '';
let confidence = titleSimilarity; let confidence = 0;
// Check if it's the same product family but different size
if (titleSimilarity > 0.8) { if (titleSimilarity > 0.8) {
if (itemSize && productSize && itemSize !== productSize) { if (itemSize && productSize) {
reason = `Same product, different size (${productSize} vs ${itemSize})`; if (itemSize === productSize) {
confidence = 0.95; // Very high confidence - likely meant to be this size variant reason = `Same product, same size (${itemSize})`;
confidence = Math.max(titleSimilarity, 0.95);
} else {
// Both sizes known and they differ — this is NOT the right product.
// Suppress it so a wrong-size variant is never suggested.
confidence = 0;
}
} else if (itemSize && !productSize) { } else if (itemSize && !productSize) {
reason = `Same product, missing size specification`; reason = `Same product (existing has no size set)`;
confidence = 0.85; confidence = 0.75;
} else if (!itemSize && productSize) { } else if (!itemSize && productSize) {
reason = `Same product, has size: ${productSize}`; reason = `Same product, has size: ${productSize}`;
confidence = 0.8; confidence = 0.7;
} else { } else {
reason = `Very similar product`; reason = `Very similar product`;
confidence = titleSimilarity; confidence = titleSimilarity;
} }
} else if (titleSimilarity > 0.6) { } else if (titleSimilarity > 0.6) {
reason = `Similar product`; // Don't suggest a different-size variant even on a looser title match
confidence = titleSimilarity; if (itemSize && productSize && itemSize !== productSize) {
confidence = 0;
} else {
reason = `Similar product`;
confidence = titleSimilarity;
}
} }
if (confidence > 0.6) { if (confidence > 0.6) {
suggestions.push({ suggestions.push({ product, confidence, reason });
product,
confidence,
reason
});
} }
} }
// Sort by confidence and return top suggestions // Sort by confidence and return top suggestions
return suggestions return suggestions
.sort((a, b) => b.confidence - a.confidence) .sort((a, b) => b.confidence - a.confidence)