fix(vinpin): stop budget-burn + seat-poison on undecodable Renault when Rpartstore down

FIX A (runRenaultFlow): when Rpartstore is UNAVAILABLE (down-cooldown or it
never loaded, so `primary` is only a placeholder ambiguous), a CLEAN Dialogys
not_found is now DEFINITIVE. The old gate required BOTH catalogs to say
not_found, so every undecodable Renault while Rpartstore was down got
downgraded to ambiguous → 3x retry → 180s budget → sessionPoisoned, which
then degraded later decodes. A genuinely-ambiguous Dialogys (unreachable)
still retries. When Rpartstore actually ran, both-must-agree is preserved.
runDialogysSearch now logs its outcome + truncated OCR header so this class
is diagnosable from prod logs.

FIX B (vinpin.constants): add old R-number + TR-badge Renault model tokens
(R5/R9/R11/R12/R19/R21/R25, Europa/Broadway/Toros/Flash). R-prefixed form
only — no bare numerics that could false-match year/engine digits.

FIX C (vin-validator extractModelYear): the position-10 year code repeats every
30 years ("T" = 1996 or 2026) with no clean VIN-only rule. New optional
{modelResolved:false} signal: for a brand-only decode of an old-shaped Renault
VIN (Renault WMI + numeric-led VDS type code) whose code pins to the current
cycle's leading edge, roll back one 30-year cycle so a ~1996 R19 isn't labelled
2026. Narrow: model-resolved or modern-shaped VINs are unchanged. Corgi's
WMI-only decoder wired to pass modelResolved:false.

Keeps never-throw / VINPIN_DECODE_BUDGET_MS / sessionPoisoned semantics and the
Fiat + working Renault paths intact. tsc clean; vinpin + corgi + extractModelYear
tests green (new tests cover A and C).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 15:39:26 +03:00
parent d7d78a77a6
commit c03e7f4079
6 changed files with 232 additions and 7 deletions

View File

@@ -257,12 +257,55 @@ const VIN_YEAR_CODE_ANCHOR: Record<string, number> = {
* tahminidir. Servis (PL24/EMEX/NHTSA vb.) bir model yılı veriyorsa HER ZAMAN o
* değer kullanılmalıdır; bu fonksiyon onun yerine geçmez.
*/
export function extractModelYear(vin: string, referenceYear?: number): number | null {
export interface ExtractModelYearOptions {
/**
* Decode SADECE markayı çözebildiyse (model çözülmediyse) `false` geçilir.
* Pozisyon-10 yıl kodu 30 yılda bir tekrarlar; "T" hem 2026 hem 1996'dır ve
* VIN-only kesin bir ayrım YOKTUR. Yalnızca decode brand-only kaldığında VE VIN
* eski Renault sayısal tip-kodu şeklindeyken, güncel döngünün ön kenarına düşen
* (yani "şimdi/gelecek yıl" gibi görünen) belirsiz bir yılı bir önceki döngüye
* çekeriz — böylece 1996 model bir R19 yanlışlıkla 2026 olmaz. Model çözülen ya
* da modern-şekilli VIN'ler ETKİLENMEZ (varsayılan davranış korunur).
*/
modelResolved?: boolean;
}
/**
* Eski Renault/Dacia (pre-2000) VIN şekli: Renault akış WMI'si (VF1/VF2/VF6/VF7
* veya Dacia UU1) + sayısal-öncüllü tip kodu (VDS ilk karakteri RAKAM — ör. "553",
* "5R4"). Modern Renault VDS tip kodları harf-öncüllüdür (RJ/RF/RH gibi), bu yüzden
* bu heuristik modern araçları dışarıda bırakır. Spec kuralı değildir — bu yüzden
* yalnızca brand-only decode ile birlikte (aşağıda) tetiklenir.
*/
function isOldRenaultShape(upperVin: string): boolean {
const isRenaultWmi = /^VF[1267]/.test(upperVin) || upperVin.startsWith("UU1");
if (!isRenaultWmi) return false;
return /^[0-9]$/.test(upperVin[3] ?? "");
}
export function extractModelYear(
vin: string,
referenceYear?: number,
opts?: ExtractModelYearOptions,
): number | null {
if (!vin || vin.length < 10) return null;
const anchor = VIN_YEAR_CODE_ANCHOR[vin.toUpperCase()[9]];
const upper = vin.toUpperCase();
const anchor = VIN_YEAR_CODE_ANCHOR[upper[9]];
if (anchor == null) return null;
const cutoff = (referenceYear ?? new Date().getFullYear()) + 1;
const ref = referenceYear ?? new Date().getFullYear();
const cutoff = ref + 1;
let year = anchor;
while (year > cutoff) year -= 30; // imkânsız geleceği bir önceki 30-yıllık döngüye çek
// 30-yıllık döngü belirsizliği (kanıtlı vaka: VF1553K05TR596166 = ~1996 Renault 19,
// pozisyon-10 "T"). Kod hem yakın-geçmiş hem ~30 yıl öncesine denk gelir ve VIN-only
// kesin bir kural yoktur. SADECE elimizde aracın eski olduğuna dair pozitif kanıt
// olduğunda — decode brand-only kaldı (model yok) VE VIN eski Renault sayısal
// tip-kodu şeklinde — güncel döngünün ön kenarına (year >= ref, yani "şimdi/gelecek")
// pinlenmiş yılı bir önceki döngüye çekeriz. Aksi halde eski bir R19 yanlış 2026 olur.
// DAR tutulur: model çözülen ya da modern-şekilli VIN'lerin yılı asla değişmez.
if (opts?.modelResolved === false && year >= ref && isOldRenaultShape(upper)) {
return year - 30;
}
return year;
}