fix: resolve VIN decode cache race, fuzzy matching, and silent failure (refs #229)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Prevent lower-confidence Gemini results from overwriting higher-confidence cache entries, add reverse-contains matching so values like "X5 xDrive35i" match DB option "X5", and show amber hint when dropdown matching fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -648,11 +648,12 @@ export class VehiclesService {
|
||||
engine_type = EXCLUDED.engine_type,
|
||||
body_type = EXCLUDED.body_type,
|
||||
raw_data = EXCLUDED.raw_data,
|
||||
cached_at = NOW()`,
|
||||
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response)]
|
||||
cached_at = NOW()
|
||||
WHERE (vin_cache.raw_data->>'confidence')::float <= $8`,
|
||||
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response), response.confidence ?? 1]
|
||||
);
|
||||
|
||||
logger.debug('VIN cached', { vin });
|
||||
logger.debug('VIN cached', { vin, confidence: response.confidence });
|
||||
} catch (error) {
|
||||
logger.error('Failed to cache VIN data', { vin, error });
|
||||
// Don't throw - caching failure shouldn't break the decode flow
|
||||
@@ -741,6 +742,12 @@ export class VehiclesService {
|
||||
const sourceEngine = response.engine;
|
||||
const sourceTransmission = response.transmission;
|
||||
|
||||
logger.debug('VIN decode raw values', {
|
||||
vin: response.vin,
|
||||
year: sourceYear, make: sourceMake, model: sourceModel,
|
||||
trim: sourceTrim, confidence: response.confidence
|
||||
});
|
||||
|
||||
// Year is always high confidence if present (exact numeric match)
|
||||
const year: MatchedField<number> = {
|
||||
value: sourceYear,
|
||||
@@ -854,6 +861,26 @@ export class VehiclesService {
|
||||
return { value: containsMatch, sourceValue, confidence: 'medium' };
|
||||
}
|
||||
|
||||
// Try reverse contains - source value contains option (e.g., source "X5 xDrive35i" contains option "X5")
|
||||
// Prefer the longest matching option to avoid false positives (e.g., "X5 M" over "X5")
|
||||
const reverseMatches = options.filter(opt => {
|
||||
const normalizedOpt = opt.toLowerCase().trim();
|
||||
return normalizedSource.includes(normalizedOpt) && normalizedOpt.length > 0;
|
||||
});
|
||||
if (reverseMatches.length > 0) {
|
||||
const bestMatch = reverseMatches.reduce((a, b) => a.length >= b.length ? a : b);
|
||||
return { value: bestMatch, sourceValue, confidence: 'medium' };
|
||||
}
|
||||
|
||||
// Try word-start match - source starts with option + separator (e.g., "X5 xDrive" starts with "X5 ")
|
||||
const wordStartMatch = options.find(opt => {
|
||||
const normalizedOpt = opt.toLowerCase().trim();
|
||||
return normalizedSource.startsWith(normalizedOpt + ' ') || normalizedSource.startsWith(normalizedOpt + '-');
|
||||
});
|
||||
if (wordStartMatch) {
|
||||
return { value: wordStartMatch, sourceValue, confidence: 'medium' };
|
||||
}
|
||||
|
||||
// No match found - return source value as hint with no match
|
||||
return { value: null, sourceValue, confidence: 'none' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user