Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Faz 3 backfill'i açmadan önce hacmi ölçtüm ve raporun işaret ettiğinden
çok daha büyük bir israf çıktı.
**Mitsubishi'nin parça listesi grup sanılıyordu.**
`/p5mitsubishi/extern/details/vinDetails` yanıtı `partno`/`qty` taşıyan
bir PARÇA listesi (canlı doğrulama: 16 kayıt), ama her kaydın kendi
linki `partInfoTable` ve ne wid ne yol sınıflandırıcıda karşılık
buluyordu. Sonuç (prod ölçümü): 2.193 parça listesi grup düğümüne
döndü, içlerindeki 19.576 tekil parça ("SCREW,LOCK CYLINDER",
"BOLT,STEERING COLUMN WASHER") kategori olarak kaydedildi. Bu 19.576
sahte düğümün TOPLAM 2 tanesinde parça var ve hepsi her prefetch
turunda yeniden çekiliyor. İkisi de %100 Mitsubishi.
- `detailsTable` artık yaprak, `isPl24PartDetailNode()` ile
`partInfoTable` hiç kuyruklanmıyor.
- `isLeafLinkPath` artık `linkWid`'i de geçiriyor. Okuma yolu bu
güvenilir sinyali hep kullanıyordu ama kuyruklama yolu düşürüyordu —
sınıflandırıcı `detailsTable`'ı öğrense bile burada yine grup
sayılacaktı.
- Migration 0036: 19.576 sahte kategori siliniyor. Okuma yolu bir
düğümün ÖNCE çocuklarına baktığı için bu silme düzeltmenin parçası,
ayrı temizlik değil. Kuru çalıştırma: 19.576 kategori, 9 araç, 2
parça, Mitsubishi dışı 0.
**Backfill anahtarı.** `PL24_BACKFILL_ENABLED` eklendi, varsayılan
KAPALI. Eski `PL24_TR_DISABLED` adı "tr hesabı öldü" diyordu ama işi
"toplu yükü tek sağ kalan hesaptan uzak tut"tu. Eski değişken hâlâ
kapatabiliyor — yarım deploy musluğu sessizce açamasın. Kullanıcı
tetikli fast-lane bu anahtardan etkilenmiyor.
9 yeni test. api 647 test geçiyor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
6.7 KiB
TypeScript
152 lines
6.7 KiB
TypeScript
/**
|
|
* One source of truth for "is this PL24 node a parts leaf or a group to drill?".
|
|
*
|
|
* WHY THIS FILE EXISTS (plv2.md, findings p5core-02 / psa-05 / consumers_jobs-01):
|
|
* the same question was answered by four independent heuristics —
|
|
* `categories.service` (twice), `catalog.service` and the prefetch worker — and
|
|
* they disagreed. Two failure modes shipped repeatedly:
|
|
*
|
|
* 1. `linkWid.includes("group")` as the "is a group" test. Live P5 PSA's second
|
|
* level has wid `illusTable`, Volvo's and Subaru's `illustrationsTable` —
|
|
* none contain "group", so those nodes fell through to the parts fetcher,
|
|
* which asked an *illustration list* for parts, got records with no partno,
|
|
* and rendered a silent empty panel (`parts: []`, no error). This is the same
|
|
* class of bug as the 2026-06 `isPsaParent` incident.
|
|
* 2. Case-sensitive `includes("/bomdetails")` while p5psa and p5volvo spell the
|
|
* endpoint `/details/vin/bomDetails` — so those leaves were queued as groups,
|
|
* `getChildren` returned [] and their parts were never prefetched (live today
|
|
* for Mitsubishi 99.9% / Fiat 80% / Renault 70% of bomDetails leaves).
|
|
*
|
|
* The reliable cross-brand marker is the response's own `link.wid`: `bomlist`
|
|
* (and the service-parts/partinfo variants) means parts, anything else means
|
|
* drill. Path matching stays as a fallback for stored rows without a wid.
|
|
*/
|
|
|
|
/**
|
|
* `link.wid` values that identify a parts (BOM) node across every P5 backend.
|
|
*
|
|
* `detailstable` is Mitsubishi's: `/p5mitsubishi/extern/details/vinDetails`
|
|
* answers with 16-ish records carrying `partno`/`qty`, i.e. it IS the parts
|
|
* list — but each record's own link is a `partInfoTable` per-part detail, and
|
|
* neither the wid nor the path matched anything here, so the whole list was
|
|
* drilled as a group. Prod on 2026-09-20: 2,193 Mitsubishi parts lists turned
|
|
* into group nodes and their 19,576 individual parts ("SCREW,LOCK CYLINDER",
|
|
* "BOLT,STEERING COLUMN WASHER") became categories — 19,576 fake tree nodes
|
|
* with 2 parts between them, each re-fetched on every prefetch pass.
|
|
*/
|
|
const LEAF_WIDS = new Set([
|
|
"bomlist",
|
|
"bomoverviewlist",
|
|
"servicepartsitemstable",
|
|
"partinfo",
|
|
"detailstable",
|
|
]);
|
|
|
|
/**
|
|
* Nodes that describe ONE part rather than a list of them. They are neither a
|
|
* group to drill nor a list to fetch: the parent's own response already carried
|
|
* the part. Queueing them buys nothing and costs one upstream request each —
|
|
* 19,576 of them on prod before this was recognised.
|
|
*/
|
|
const PART_DETAIL_WIDS = new Set(["partinfotable"]);
|
|
const PART_DETAIL_PATH = /\/details\/vinpartinfo\b/i;
|
|
|
|
/** True when this node is a single part's detail view, not a listing. */
|
|
export function isPl24PartDetailNode(opts: {
|
|
linkPath?: string | null;
|
|
linkWid?: string | null;
|
|
}): boolean {
|
|
const wid = opts.linkWid?.toLowerCase().trim();
|
|
if (wid && PART_DETAIL_WIDS.has(wid)) return true;
|
|
return PART_DETAIL_PATH.test(opts.linkPath ?? "");
|
|
}
|
|
|
|
/** `link.wid` values that identify a drillable group node. */
|
|
const GROUP_WID_PATTERN =
|
|
/(group|scope|illus|illustration|catalog|model|vpages|msppages|chemicals|category|categories)/i;
|
|
|
|
/**
|
|
* Path fragments that only ever appear on a parts endpoint. `image-board` has no
|
|
* leading slash on purpose: the legacy PSA leaf is `vin-image-board.action`.
|
|
*/
|
|
const LEAF_PATH_PATTERN =
|
|
/\/(bom|bomdetails|partinfo|vin_items|mdl_items|vin_bomdetails)\b|\/bom\/|\/details\/vin\/bomdetails|\/servicepart\/vin_items|image-board/i;
|
|
|
|
/** True when this node yields parts (never children). */
|
|
export function isPl24LeafNode(opts: {
|
|
linkPath?: string | null;
|
|
linkWid?: string | null;
|
|
hasSubgroups?: boolean | null;
|
|
}): boolean {
|
|
const wid = opts.linkWid?.toLowerCase().trim();
|
|
if (wid) {
|
|
if (LEAF_WIDS.has(wid)) return true;
|
|
if (GROUP_WID_PATTERN.test(wid)) return false;
|
|
}
|
|
// Explicit DB hint wins over path guessing when there is no usable wid.
|
|
if (opts.hasSubgroups === true) return false;
|
|
const lp = opts.linkPath?.toLowerCase() ?? "";
|
|
if (!lp) return false;
|
|
return LEAF_PATH_PATTERN.test(lp);
|
|
}
|
|
|
|
/** True when this node should be drilled for children. */
|
|
export function isPl24GroupNode(opts: {
|
|
linkPath?: string | null;
|
|
linkWid?: string | null;
|
|
hasSubgroups?: boolean | null;
|
|
}): boolean {
|
|
if (!opts.linkPath && !opts.linkWid) return false;
|
|
// A per-part detail node is not a group; drilling it returns nothing.
|
|
if (isPl24PartDetailNode(opts)) return false;
|
|
return !isPl24LeafNode(opts);
|
|
}
|
|
|
|
/**
|
|
* True when a stored vehicle's catalogInfo points at an architecture the service
|
|
* no longer uses — i.e. the row was decoded before PSA/Volvo moved to P5.
|
|
*
|
|
* These vehicles keep serving a tree built from the frozen P4 PSA snapshot
|
|
* (upds 2024-02-13) or the dead P4 Volvo endpoint (HTTP 503), and `decodeVin`'s
|
|
* db_hit short-circuit means a code fix never reaches them. Detecting the
|
|
* mismatch at read time lets each vehicle heal itself on first view instead of
|
|
* needing a bulk re-decode storm against the one surviving account.
|
|
*/
|
|
export function isStalePl24Architecture(opts: {
|
|
catalogPath?: string | null;
|
|
currentApiPath?: string | null;
|
|
}): boolean {
|
|
const stored = opts.catalogPath?.toLowerCase() ?? "";
|
|
const current = opts.currentApiPath?.toLowerCase() ?? "";
|
|
if (!stored || !current) return false;
|
|
// Only the two migrated legacy backends; unknown/other paths are left alone.
|
|
const storedIsLegacyPsaOrVolvo = stored.startsWith("/psa") || stored.startsWith("/volvo");
|
|
if (!storedIsLegacyPsaOrVolvo) return false;
|
|
return current.startsWith("/p5");
|
|
}
|
|
|
|
/**
|
|
* True when a stored `catalog_vehicles` browse row was listed under an
|
|
* architecture the service table no longer uses.
|
|
*
|
|
* Browse rows are a PERMANENT cache: `CatalogService.getModels` returns early
|
|
* whenever the brand already has rows, so `fetchVehicleList` is never called
|
|
* again for that brand. The 188 rows listed while PSA and Volvo were still P4
|
|
* (127 LEGACY_PSA + 61 LEGACY_VOLVO on prod, 2026-09-20) are therefore pinned
|
|
* forever: Peugeot/Citroën browse serves the frozen 2024-02-13 snapshot and
|
|
* Volvo/Polestar browse serves an endpoint that answers HTTP 503. Detecting the
|
|
* mismatch at list time lets the brand re-list itself once, the same way
|
|
* `isStalePl24Architecture` heals a VIN-decoded vehicle.
|
|
*/
|
|
export function isStaleBrowseArchitecture(opts: {
|
|
storedArchitecture?: string | null;
|
|
currentArchitecture?: string | null;
|
|
}): boolean {
|
|
const stored = opts.storedArchitecture?.trim();
|
|
const current = opts.currentArchitecture?.trim();
|
|
// An unknown service (no config) or an unlabelled row is left alone: without a
|
|
// current architecture to compare against there is nothing to migrate TO.
|
|
if (!stored || !current) return false;
|
|
return stored !== current;
|
|
}
|