From 0431f05845dfe37fd6a5ef156791f066959fe198 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:17:03 +0700 Subject: [PATCH 1/3] fix(normalizer): hp folio laptops keep a key despite the accessory opener isAccessoryTitle read "Folio" as a folio case, so a title such as "Folio 13 HP Core i5 8GB" got an unkeyed key. A line word that is also an accessory word now names the device when a model number follows it, the title names the line's brand (HP) and no fit word ("cho", "for") or other accessory word (case, sleeve, bao da) follows. Folio cases for iPad or Samsung, and "Folio 13 cho HP", stay accessories. Closes #1539 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/open-core-intelligence.test.ts | 12 +++++++ .../importer/src/shopee/product-normalizer.ts | 33 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts index b437c999..5d43aa86 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -714,6 +714,13 @@ async function runSuite() { 'Dây Apple Watch S/M 42mm', 'Dây Apple Watch Series 10 46mm tặng ốp', '🔥 [Chính hãng] Ốp lưng iPhone 16 Pro Max', + // DOS/Rate#1539: a folio case stays a case; only HP's Folio line is a device. + 'Folio case for iPad Pro 11', + 'Folio 11 inch cho iPad Pro', + 'Folio iPad 10', + 'Folio 13 cho HP Pavilion', + 'Folio 13 HP sleeve chống sốc', + 'Folio 13 Samsung Galaxy Tab S9', ]; for (const title of accessoryTitles1505) { const accessory = normalizer.normalize(createDummyShopeeProduct(title)); @@ -734,6 +741,11 @@ async function runSuite() { ['Bút cảm ứng cao cấp chính hãng Apple Pencil Pro', 'apple:pencil-pro:unknown'], // A gift that opens the title is not what it sells. ['Ốp lưng tặng kèm khi mua iPhone 16 Pro Max 256GB', 'apple:iphone-16-pro-max:256gb'], + // DOS/Rate#1539: HP's Folio laptops open with the line word and keep a key. The + // brand after the model stays in it, as for any laptop line typed that way. + ['Folio 13 HP Core i5 8GB 256GB', 'hp:folio-13-hp-core:256gb'], + ['[Chính hãng] Folio 13 HP i5 8GB', 'hp:folio-13-hp-i5:8gb'], + ['HP EliteBook Folio 1040 G3 i7 16GB', 'hp:elitebook-folio-1040-g3:16gb'], ] as const) { assert.equal(keyOf(title), expectedKey, title); } diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 713209d4..13cd35b2 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -411,6 +411,31 @@ const ACCESSORY_OPENER = const PENCIL_FIT_WORD = /(? = [ + [/^folio\s+\d{1,4}[a-z]?(?![\p{L}\d])/iu, /(? { + const head = line.exec(opening); + if (!head || !brand.test(opening)) return false; + const rest = opening.slice(head[0].length); + return !PENCIL_FIT_WORD.test(rest) && !ACCESSORY_WORD.test(rest); + }); +} + /** * A pen sold as itself: "Bút cảm ứng (chính hãng) Apple Pencil Pro" is the Apple Pencil, * its own product. Only the pen word and promo words may come before "Apple Pencil"; a @@ -455,7 +480,13 @@ export function isAccessoryTitle(title: string): boolean { // What the title sells, not a gift it opens with: "Ốp lưng tặng kèm khi mua iPhone 16 Pro // Max" sells the iPhone (DOS/Rate#1407). const opening = openingWords(ownProductPart(t)); - if (!ACCESSORY_OPENER.test(opening) || opensWithApplePencil(opening)) return false; + if ( + !ACCESSORY_OPENER.test(opening) || + opensWithApplePencil(opening) || + opensWithDeviceLine(opening) + ) { + return false; + } // The part before a gift stops at the "+", so the bundle is read on the whole title. const whole = openingWords(t); const plus = whole.indexOf('+'); From 58d6a67cf4e2d782aa6c1082df17e192e50b7dd3 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:23:30 +0700 Subject: [PATCH 2/3] fix(normalizer): read only the word after the hp brand for folio titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #1541. The first version scanned the whole title after the line word for accessory and fit words, so a laptop's own text ("bàn phím có đèn", "cho sinh viên") unkeyed it, while diacritic-less accessories ("op lung", "tui chong soc") got a key. Now HP must come right after the Folio model (13, 1040 G3, G1), and only the word right after the brand is checked against the accessory openers, laptop accessories (sleeve, adapter, túi) and fit words (cho, for, tương thích). Promo words before the line ("Combo", "Hàng chính hãng") are read as ACCESSORY_OPENER reads them. Refs #1539 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/open-core-intelligence.test.ts | 22 +++++++++++ .../importer/src/shopee/product-normalizer.ts | 37 +++++++++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts index 5d43aa86..92eb756c 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -721,6 +721,14 @@ async function runSuite() { 'Folio 13 cho HP Pavilion', 'Folio 13 HP sleeve chống sốc', 'Folio 13 Samsung Galaxy Tab S9', + // Review of #1541: an accessory or fit word right after the brand, and a brand that is + // not right after the model. + 'Folio 13 HP op lung', + 'Folio 13 HP tui chong soc', + 'Folio 13 HP cường lực', + 'Folio 13 HP adapter 65W', + 'Folio 13 HP tuong thich', + 'Folio 12 iPad Pro HP', ]; for (const title of accessoryTitles1505) { const accessory = normalizer.normalize(createDummyShopeeProduct(title)); @@ -749,6 +757,20 @@ async function runSuite() { ] as const) { assert.equal(keyOf(title), expectedKey, title); } + // Review of #1541: promo words before the line, older "G1" models, and the laptop's own + // text later in the title ("bàn phím có đèn", "dùng cho văn phòng") keep the device. + for (const title of [ + '[Hàng chính hãng] Folio 13 HP i5 8GB', + 'Combo Folio 13 HP i5 8GB', + 'Folio G1 HP Core m5 8GB', + 'Folio 1040 G3 HP i5 8GB', + 'Folio 13 (HP) i5 8GB', + 'Folio 13 HP Core i5 bàn phím có đèn', + 'Folio 13 HP i5 dùng cho văn phòng', + 'Folio 13 HP pin 8 tiếng', + ]) { + assert.equal(normalizer.normalize(createDummyShopeeProduct(title)).keyable, true, title); + } // DOS/Rate#1124: a size outside a band phrase is the case's; a band's size counts only // when the title names no other. const watchBandSizeKeys1124: Array<[string, string]> = [ diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 13cd35b2..3e59b690 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -411,28 +411,33 @@ const ACCESSORY_OPENER = const PENCIL_FIT_WORD = /(? = [ - [/^folio\s+\d{1,4}[a-z]?(?![\p{L}\d])/iu, /(? { - const head = line.exec(opening); - if (!head || !brand.test(opening)) return false; - const rest = opening.slice(head[0].length); - return !PENCIL_FIT_WORD.test(rest) && !ACCESSORY_WORD.test(rest); + const title = opening.replace(OPENER_PROMO, ''); + return DEVICE_LINE_OPENERS.some(line => { + const head = line.exec(title); + if (!head) return false; + const rest = title.slice(head[0].length).trim(); + return !ACCESSORY_OPENER.test(rest) && !LAPTOP_ACCESSORY_OPENER.test(rest); }); } From ff19546637f40c592759d8f9ae661804f54186d3 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:27:20 +0700 Subject: [PATCH 3/3] fix(normalizer): catch diacritic-less and dashed accessories after folio hp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round 2 on #1541. "Folio 13 HP sac 45W" and "Folio 13 HP ban phim US" got a key, and a dash or bracket before the accessory ("Folio 13 HP - sleeve") hid it. The word after the brand is now read past punctuation, and sac, cap and ban phim join the laptop accessory list. "cho" leaves it, so "Folio 13 HP cho sinh viên Core i5" keeps a key. Refs #1539 Co-Authored-By: Claude Opus 5.5 --- .../shopee/__tests__/open-core-intelligence.test.ts | 5 +++++ apps/importer/src/shopee/product-normalizer.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts index 92eb756c..c97c40a2 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -729,6 +729,10 @@ async function runSuite() { 'Folio 13 HP adapter 65W', 'Folio 13 HP tuong thich', 'Folio 12 iPad Pro HP', + // Codex round 2 on #1541. + 'Folio 13 HP sac 45W dau kim', + 'Folio 13 HP ban phim US thay the', + 'Folio 13 HP - sleeve da màu nâu', ]; for (const title of accessoryTitles1505) { const accessory = normalizer.normalize(createDummyShopeeProduct(title)); @@ -768,6 +772,7 @@ async function runSuite() { 'Folio 13 HP Core i5 bàn phím có đèn', 'Folio 13 HP i5 dùng cho văn phòng', 'Folio 13 HP pin 8 tiếng', + 'Folio 13 HP cho sinh viên Core i5 4GB RAM SSD 128GB', ]) { assert.equal(normalizer.normalize(createDummyShopeeProduct(title)).keyable, true, title); } diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 3e59b690..d5392bda 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -419,10 +419,11 @@ const DEVICE_LINE_OPENERS: readonly RegExp[] = [ ]; // The promo words ACCESSORY_OPENER lets come before the accessory. const OPENER_PROMO = /^(?:(?:combo|bộ|set|hàng\s*chính\s*hãng)\s+)?/iu; -// Accessories that ACCESSORY_OPENER does not name, and fit words, right after a laptop name: -// "Folio 13 HP sleeve", "Folio 13 HP tui chong soc", "Folio 13 HP adapter 65W". +// Accessories that ACCESSORY_OPENER does not name, typed with or without diacritics, and +// fit words, right after a laptop name: "Folio 13 HP sleeve", "Folio 13 HP tui chong soc", +// "Folio 13 HP sac 45W". "cho" is not one: "Folio 13 HP cho sinh viên" is the laptop. const LAPTOP_ACCESSORY_OPENER = - /^(?:sleeves?|pouch|bags?|balo|ba\s*lô|túi|tui|bao|adapter|battery|decal|stand|casing|miếng|mieng|kinh|dan|tempered|tương\s*thích|tuong\s*thich|compatible|for|cho|thay)(?![\p{L}\d])/iu; + /^(?:sleeves?|pouch|bags?|balo|ba\s*lô|túi|tui|bao|adapter|battery|decal|stand|casing|miếng|mieng|kinh|dan|sac|cap|ban\s*phim|tempered|tương\s*thích|tuong\s*thich|compatible|thay)(?![\p{L}\d])/iu; /** * A device whose line name is also an accessory word: "Folio 13 HP ..." is an HP Folio @@ -436,7 +437,8 @@ function opensWithDeviceLine(opening: string): boolean { return DEVICE_LINE_OPENERS.some(line => { const head = line.exec(title); if (!head) return false; - const rest = title.slice(head[0].length).trim(); + // "Folio 13 HP - sleeve da" and "Folio 13 HP (sleeve)" read the word after the dash. + const rest = title.slice(head[0].length).replace(/^[\s\p{P}\p{S}]+/u, ''); return !ACCESSORY_OPENER.test(rest) && !LAPTOP_ACCESSORY_OPENER.test(rest); }); }