優化 PChome 近門檻商品比對回收
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-25 15:20:36 +08:00
parent a00f34ce87
commit dab782cc6d
7 changed files with 399 additions and 12 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.474 補 PChome near-threshold matcher / feeder 下一階段:新增 HOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 的窄範圍 total-price exact 回收Recipe Box 可撕式水性兒童指甲油只進 identity_review不自動寫正式價差Pavaruni 蠟燭 vs 精油、DASHING DIVA 不同款式仍維持 veto/低信心。known-id refresh 現在會對 hard-veto 舊候選執行 fresh search recoverymissing known-id 若 fresh search 只找到低分候選也會保留 best candidate + diagnostics而非落成 `refresh_no_result`;正式覆寫保護新增 stronger existing guard避免較弱新候選以高分覆蓋既有強正式配對。測試`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
- V10.473 補背景 embedding host_health skip`allow_111_fallback=false` 會讀最近 `host_health_probes`,跳過 runtime unhealthy 的 GCP 節點(預設 20 分鐘DB fail-open避免每筆任務都等待已知壞節點 timeout路由安全不變不把背景 embedding 落 111。
- V10.472 補 GCP Ollama failover rootless 診斷:新增 `scripts/ops/diagnose_ollama_gcp_failover.sh`,可一鍵檢查 GCP-A direct、GCP-B direct、111、110:11435、110:11436 與 GCP-B `bge-m3` runtime目前輸出確認 GCP-A `11434` refused、GCP-B direct/embed OK、110:11435 502、110:11436 OK。110 無免密 sudo、GCP-A 22 refused、GCP-B SSH key denied因此 primary 修復需 GCP/SSH 或 110 root 權限;應用層維持 GCP-A → GCP-B → 111不把背景 embedding 落 111。
- V10.471 依 GCP-B `bge-m3` 實測 latency 調整 embedding timeout已部署正式環境並確認 `/health=V10.471`GCP-B `/api/embed` 三次實測約 6.4s / 7.3s / 23.5s,原本 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `8s` 會誤殺慢但成功的 embedding已將背景 embedding cap 與 host health model probe timeout 預設調為 30s。正式 smoke 顯示容器內 embedding 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。路由安全不變背景 embedding 仍只跑 GCP-A/GCP-B不落 111。

View File

@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.473"
SYSTEM_VERSION = "V10.474"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -27,6 +27,7 @@
- 2026-05-25 13:38 CST 狀態:已部署 `V10.471` 到 188正式 `/health``V10.471`。本輪 recreate `momo-app``scheduler``telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、首頁 / daily / growth / host_health / ppt_audit_history / PChome review queue HTTP 200。GCP-B `bge-m3` `/api/embed` 直接實測約 6.4s、7.3s、23.5s,原 `OLLAMA_EMBED_MAX_TIMEOUT=15` 與 host health `OLLAMA_HOST_HEALTH_EMBED_TIMEOUT=8` 會誤殺慢但成功的 embedding預設改為 30s。正式容器內 embedding smoke 回 1024 維、耗時 10.07s;手動 host health probe 後最新狀態為 GCP-A unhealthy、GCP-B healthy、111 healthy。背景 embedding 路由安全不變GCP-A → GCP-B不落 111。
- 2026-05-25 14:10 CST 起,`V10.472` 補 rootless GCP Ollama failover 診斷腳本與 DevOps SOP`scripts/ops/diagnose_ollama_gcp_failover.sh` 會檢查 direct GCP-A/GCP-B/111、110 proxy `11435/11436` 與 GCP-B `bge-m3` runtime。現況輸出GCP-A direct `/api/version` failed/refused、GCP-B direct OK、111 OK、110:11435 502、110:11436 OK、GCP-B embed OK110 無免密 sudo`ssh gcp-a` 22 refused、`ssh gcp-b` publickey denied因此 primary 修復需 GCP/SSH 或 110 root 權限。
- 2026-05-25 14:12 CST 起,`V10.473` 進行背景 embedding host_health skip`allow_111_fallback=false` 的背景 embedding 會讀最近 `host_health_probes`,若 GCP-A/GCP-B runtime 已被標 unhealthy直接跳過該節點並開 GCP circuit不等待 30 秒 timeout、不落 111DB 讀取失敗 fail-open。
- 2026-05-25 14:45 CST 起,`V10.474` 補 PChome near-threshold matcher / feeder 下一階段HOOOME 白色經典香氛暖燈、Gdesign Aroma Lava 2.0 進 total-price exactRecipe Box 可撕式水性兒童指甲油保留 identity_review不自動寫正式價差Pavaruni 蠟燭 vs 精油與 DASHING DIVA 不同款式仍不放行。known-id refresh 會對 hard-veto 舊候選跑 fresh search recoverymissing known-id 若 fresh search 只有低分候選,也保留 best candidate + diagnostics不再只記 `refresh_no_result`;正式覆寫保護新增 stronger existing guard。
- 2026-05-25 12:05 CST 狀態:`main` 已部署到 188正式 `/health``V10.467`,待推 Gitea。兩段變更已合併驗證V10.466 rescore duplicate 改看 latest-state7 筆 SKU 最新 attempt 全為 `rescore_accepted_current``competitor_prices` / `competitor_price_history` 目標計數未變V10.467 focused exact matcher 在容器內回 `exact / total_price / price_alert_exact`。本輪 recreate `momo-app``scheduler``telegram-bot`;未使用 `--remove-orphans`,未碰 `momo-db`。Smoke 通過:三容器 healthy、PChome rescore queue API HTTP 200、Gemini 24 小時無 provider 紀錄、Ollama env 順序維持 GCP-A → GCP-B → 111、3 分鐘三容器 log 未見 Traceback / ERROR / CRITICAL / IntegrityError。
## 1. MOMO / PChome 核心比價準確率
@@ -64,6 +65,7 @@
- 2026-05-25 11:55 CST 起rescore audit duplicate 判斷只看最新 attempt若歷史已有 accepted 但後續 crawler 又追加低信心列,可重新 materialize 成最新 `rescore_accepted_current`。Production pilot 已將 SKU `14756069``11159042``13842560``8394210``15192547``10509765``10603780` 入人工覆核隊列;正式 `competitor_prices` / `competitor_price_history` 未寫入或改變。
- 2026-05-25 12:20 CST 起matcher 新增 `focused_exact_total_price_safe` 窄範圍通道;目前只覆蓋 3W CLINIC 粉底液 2入、花美水凝膠 3支、The Ordinary 咖啡因 EGCG 30ml、KUSSEN 屁屁膏 3入、Bone 擴香禮盒、1990 融燭燈白色款與 CANMAKE 淚袋盤等已確認同款樣本。這讓高信心 `exact/manual_review` 能轉為 `exact/total_price` 供 rescore pilot 入人工覆核DASHING DIVA、唇彩、香味、色號/款式敏感商品仍不放行。
- 2026-05-25 12:25 CST production pilotSKU `6101639``10074951``7760902``TP00074980000005``14774766``10142589``10262470``10262471``11308520` 已從 `true_low_confidence` materialize 為 `rescore_accepted_current`,全數 `exact/total_price/price_alert_exact` 且理由含 `focused_exact_total_price_safe`。SKU `6101784` 因「即期品」商業條件不同,刻意保留在 `true_low_confidence`,不納入本輪自動入隊。
- 2026-05-25 14:45 CST 起matcher 擴充至香氛/精油近門檻安全 cohortHOOOME 白色經典香氛暖燈與 Gdesign Aroma Lava 2.0 可進 `exact/total_price/price_alert_exact`Recipe Box 可撕式水性兒童指甲油只進 `identity_review`,因兒童指甲油仍可能藏色款/款式。DASHING DIVA 與 Pavaruni cross-type 負例已補測試,避免跨款式、跨劑型誤配。
## 3. 12 Agent 決策信封整合

View File

@@ -1371,6 +1371,13 @@ class CompetitorPriceFeeder:
"replace_same_identity_better_score="
f"{existing_score:.3f}->{match_score:.3f}",
)
if existing_score >= match_score:
return (
False,
f"existing_match_conflict;stronger_existing;existing_id={existing_id};"
f"incoming_id={incoming_id};existing_score={existing_score:.3f};"
f"incoming_score={match_score:.3f}",
)
if match_score >= REPLACE_DIFFERENT_PRODUCT_SCORE:
return True, f"replace_high_confidence_score={match_score:.3f}"
return (
@@ -1937,6 +1944,35 @@ class CompetitorPriceFeeder:
attempts_written += 1
continue
attempt_status = _classify_low_score_attempt(score, diagnostics)
if (
attempt_status == "recoverable_low_score"
and _has_variant_selection_gap(momo_name, [(best_product, score, diagnostics)], score)
):
attempt_status = "true_low_confidence"
attempt_terms = search_terms + [term for term in recovery_terms if term not in search_terms]
self._record_match_attempt(
sku,
momo_name,
momo_product_id=momo_product_id,
momo_price=momo_price,
search_terms=attempt_terms,
candidate_count=max(1, recovery_candidate_count),
attempt_status=attempt_status,
best_product=best_product,
best_score=score,
diagnostics=diagnostics,
error_message=(
"missing_known_product_id_fresh_search_low_confidence; "
f"PChome product_id not returned: {competitor_product_id}; "
f"{_format_match_diagnostics(diagnostics)}"
),
source=source,
)
skipped_low += 1
attempts_written += 1
continue
self._record_match_attempt(
sku,
momo_name,
@@ -1991,17 +2027,20 @@ class CompetitorPriceFeeder:
if score < MIN_MATCH_SCORE:
recovery_terms: list[str] = []
recovery_candidate_count = 0
if not getattr(diagnostics, "hard_veto", False):
recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search(
crawler,
momo_name,
momo_price=momo_price,
existing_product_id=competitor_product_id,
)
if recovered:
recovered_product, recovered_score, recovered_diagnostics = recovered
if recovered_score > score:
best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics
recovered, recovery_terms, recovery_candidate_count = _recover_low_score_with_fresh_search(
crawler,
momo_name,
momo_price=momo_price,
existing_product_id=competitor_product_id,
)
if recovered:
recovered_product, recovered_score, recovered_diagnostics = recovered
if (
recovered_score > score
or getattr(diagnostics, "hard_veto", False)
and not getattr(recovered_diagnostics, "hard_veto", False)
):
best_product, score, diagnostics = recovered_product, recovered_score, recovered_diagnostics
if score >= MIN_MATCH_SCORE:
extras = ["refresh_known_identity"]

View File

@@ -365,6 +365,9 @@ SEARCH_IDENTITY_ANCHORS = (
"天然植物香氛精油",
"爆水擦澡濕巾",
"嬰兒潤膚乳",
"可撕式水性兒童指甲油",
"aroma lava 解憂放鬆緩緩燈",
"經典款香氛蠟燭暖燈",
"我愛超磁妝定妝噴霧",
"全天候超完美定妝噴霧",
"怪獸級持色唇膏",
@@ -475,6 +478,7 @@ FOCUSED_IDENTITY_REVIEW_ONLY_REASONS = {
"kate_monster_lipstick_catalog",
"opi_gel_polish_series_catalog",
"romand_juicy_lip_tint_2_catalog",
"recipe_box_peelable_child_polish_catalog",
"solone_longlasting_eyeliner",
"shu_auto_hard_formula_refill_catalog",
"summer_eve_full_skin_wash_2pack",
@@ -497,6 +501,8 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = {
"selection1990_half_dome_wax_lamp_white",
"selection1990_bendable_wax_lamp_white",
"canmake_tear_bag_palette",
"gdesign_aroma_lava_lamp_2",
"hooome_classic_white_wax_lamp_bulbs_giftbox",
}
SEARCH_BROAD_ANCHORS = {
@@ -3512,6 +3518,38 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and "淚袋眼影盤" in right_text
):
return "canmake_tear_bag_palette"
if (
{"recipe", "box"} <= brand_tokens
and "可撕式水性兒童指甲油" in left_text
and "可撕式水性兒童指甲油" in right_text
):
return "recipe_box_peelable_child_polish_catalog"
if (
"gdesign" in (left.brand_tokens & right.brand_tokens)
and "aroma" in left_text
and "aroma" in right_text
and "lava" in left_text
and "lava" in right_text
and "解憂放鬆緩緩燈2.0" in left_text
and "解憂放鬆緩緩燈2.0" in right_text
and "熔岩燈" in left_text
and "熔岩燈" in right_text
and "精油擴香" in left_text
and "精油擴香" in right_text
):
return "gdesign_aroma_lava_lamp_2"
if (
"hooome" in (left.brand_tokens & right.brand_tokens)
and "白色" in left_text
and "白色" in right_text
and "香氛蠟燭暖燈" in left_text
and "香氛蠟燭暖燈" in right_text
and "兩顆燈泡" in left_text
and "兩顆燈泡" in right_text
and "禮盒" in left_text
and "禮盒" in right_text
):
return "hooome_classic_white_wax_lamp_bulbs_giftbox"
return ""

View File

@@ -1658,6 +1658,251 @@ def test_competitor_feeder_refresh_recovers_when_known_id_missing(monkeypatch):
assert attempts[0]["attempt_status"] == "matched"
def test_competitor_feeder_records_missing_known_id_low_score_candidate(monkeypatch):
from services.competitor_price_feeder import CompetitorPriceFeeder
from services.pchome_crawler import PChomeProduct
candidate = PChomeProduct(
product_id="DDAB01-LOW",
name="Recipe Box 韓國 recipebox 可撕式水性兒童指甲油",
price=299,
original_price=350,
discount=15,
image_url="",
product_url="https://24h.pchome.com.tw/prod/DDAB01-LOW",
stock=20,
store="24h",
rating=4.8,
review_count=8,
is_on_sale=True,
crawled_at=datetime.now(),
)
class FakeCrawler:
def __init__(self, *_args, **_kwargs):
pass
def fetch_product_details(self, product_ids, batch_size=20):
assert product_ids == ["DDAB01-MISSING"]
return True, "ok", []
def search_products(self, *_args, **_kwargs):
return True, "ok", [candidate]
def fake_score(*_args, **_kwargs):
return SimpleNamespace(
score=0.742,
brand_score=1.0,
token_score=0.66,
spec_score=0.55,
sequence_score=0.60,
type_score=0.55,
price_penalty=0.0,
hard_veto=False,
reasons=("shared_identity_anchor_variant_safe",),
comparison_mode="exact_identity",
match_type="no_match",
price_basis="none",
alert_tier="suppress",
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
)
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
feeder = CompetitorPriceFeeder(engine=object())
attempts = []
monkeypatch.setattr(
feeder,
"_record_match_attempt",
lambda *args, **kwargs: attempts.append(kwargs),
)
result = feeder._run_known_identity_refresh_items([{
"sku": "TP00018610000157",
"name": "韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)",
"product_id": 1,
"momo_price": 299,
"competitor_product_id": "DDAB01-MISSING",
}])
assert result.matched == 0
assert result.skipped_low_score == 1
assert attempts[0]["attempt_status"] == "recoverable_low_score"
assert attempts[0]["best_product"].product_id == "DDAB01-LOW"
assert "missing_known_product_id_fresh_search_low_confidence" in attempts[0]["error_message"]
def test_competitor_feeder_refresh_recovers_when_known_id_is_hard_veto(monkeypatch):
from services.competitor_price_feeder import CompetitorPriceFeeder
from services.pchome_crawler import PChomeProduct
stale = PChomeProduct(
product_id="DDAB01-STALE",
name="Pavaruni 香氛蠟燭500g",
price=980,
original_price=1200,
discount=18,
image_url="",
product_url="https://24h.pchome.com.tw/prod/DDAB01-STALE",
stock=20,
store="24h",
rating=4.7,
review_count=8,
is_on_sale=True,
crawled_at=datetime.now(),
)
recovered = PChomeProduct(
product_id="DDAB01-RECOVERED",
name="Pavaruni 天然植物香氛精油40種香味10ml",
price=399,
original_price=499,
discount=20,
image_url="",
product_url="https://24h.pchome.com.tw/prod/DDAB01-RECOVERED",
stock=20,
store="24h",
rating=4.8,
review_count=8,
is_on_sale=True,
crawled_at=datetime.now(),
)
class FakeCrawler:
def __init__(self, *_args, **_kwargs):
pass
def fetch_product_details(self, product_ids, batch_size=20):
assert product_ids == ["DDAB01-STALE"]
return True, "ok", [stale]
def search_products(self, *_args, **_kwargs):
return True, "ok", [stale, recovered]
def fake_score(_momo_name, competitor_name, **_kwargs):
if "RECOVERED" in competitor_name or "天然植物香氛精油" in competitor_name:
return SimpleNamespace(
score=0.84,
brand_score=1.0,
token_score=0.78,
spec_score=1.0,
sequence_score=0.70,
type_score=1.0,
price_penalty=0.0,
hard_veto=False,
reasons=("shared_identity_anchor",),
comparison_mode="exact_identity",
match_type="exact",
price_basis="total_price",
alert_tier="price_alert_exact",
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
)
return SimpleNamespace(
score=0.32,
brand_score=1.0,
token_score=0.20,
spec_score=0.0,
sequence_score=0.20,
type_score=0.0,
price_penalty=0.0,
hard_veto=True,
reasons=("type_conflict",),
comparison_mode="not_comparable",
match_type="no_match",
price_basis="none",
alert_tier="suppress",
tags=["identity_v2", "identity_veto"],
)
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
feeder = CompetitorPriceFeeder(engine=object())
attempts = []
writes = []
monkeypatch.setattr(
feeder,
"_should_upsert_competitor_price",
lambda *_args, **_kwargs: (True, "same_or_empty_existing"),
)
monkeypatch.setattr(
feeder,
"_upsert_competitor_price",
lambda sku, product, score, tags, **kwargs: writes.append({
"sku": sku,
"product_id": product.product_id,
"score": score,
"tags": tags,
**kwargs,
}),
)
monkeypatch.setattr(
feeder,
"_record_match_attempt",
lambda *args, **kwargs: attempts.append(kwargs),
)
result = feeder._run_known_identity_refresh_items([{
"sku": "PAVARUNI-OIL",
"name": "【Pavaruni】天然植物香氛精油40種香味10ml",
"product_id": 1,
"momo_price": 399,
"competitor_product_id": "DDAB01-STALE",
}])
assert result.matched == 1
assert writes[0]["product_id"] == "DDAB01-RECOVERED"
assert "fresh_search_recovery" in writes[0]["tags"]
assert attempts[0]["attempt_status"] == "matched"
def test_should_upsert_protects_stronger_existing_identity_candidate():
from sqlalchemy import create_engine, text
from services.competitor_price_feeder import CompetitorPriceFeeder
engine = create_engine("sqlite:///:memory:")
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE competitor_prices (
sku TEXT,
source TEXT,
competitor_product_id TEXT,
competitor_product_name TEXT,
match_score REAL,
tags TEXT
)
"""))
conn.execute(text("""
INSERT INTO competitor_prices (
sku, source, competitor_product_id, competitor_product_name, match_score, tags
) VALUES (
'14133077',
'pchome',
'DDAB01-STRONG',
'PONY EFFECT 絕對持久定妝噴霧 100ml',
0.950,
'["identity_v2","comparison_exact_identity","brand_match"]'
)
"""))
feeder = CompetitorPriceFeeder(engine=engine)
product = SimpleNamespace(
product_id="DDAB01-WEAKER",
name="PONY EFFECT 絕對持久定妝噴霧",
)
should_write, reason = feeder._should_upsert_competitor_price(
"14133077",
product,
0.850,
source="pchome",
)
assert should_write is False
assert "stronger_existing" in reason
assert "existing_score=0.950" in reason
assert "incoming_score=0.850" in reason
def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch):
from services.competitor_price_feeder import CompetitorPriceFeeder
from services.pchome_crawler import PChomeProduct

View File

@@ -2125,6 +2125,68 @@ def test_marketplace_matcher_promotes_lactacyd_private_wash_exact_line():
assert "shared_identity_anchor" in diagnostics.reasons
def test_marketplace_matcher_promotes_safe_aroma_lamps_to_total_price():
from services.marketplace_product_matcher import score_marketplace_match
hooome = score_marketplace_match(
"【HOOOME】白色 經典款香氛蠟燭暖燈 薰香燈 蠟燭燈 香氛燈 燭燈 融蠟燈(可調光+附兩顆燈泡+精美禮盒包裝 /",
"【HOOOME】白色 經典款香氛蠟燭暖燈 蠟燭燈 香氛燈 融燭燈 融蠟燈 (可調光+附兩顆燈泡+禮盒包裝)",
momo_price=1290,
competitor_price=1290,
)
gdesign = score_marketplace_match(
"【Gdesign】全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)",
"全新升級 Aroma Lava 解憂放鬆緩緩燈2.0|熔岩燈、精油擴香 2 in 1(熔岩燈/精油/氛圍燈)",
momo_price=1980,
competitor_price=1980,
)
for diagnostics in (hooome, gdesign):
assert diagnostics.score >= 0.76
assert diagnostics.hard_veto is False
assert diagnostics.match_type == "exact"
assert diagnostics.price_basis == "total_price"
assert diagnostics.alert_tier == "price_alert_exact"
assert "focused_exact_identity_hooome_classic_white_wax_lamp_bulbs_giftbox" in hooome.reasons
def test_marketplace_matcher_keeps_recipe_box_child_polish_catalog_in_review():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"韓國 recipebox 可撕式水性兒童指甲油(兒童水性指甲油 可撕式指甲油 韓兔指甲油)",
"【Recipe Box】韓國 recipebox 可撕式水性兒童指甲油",
momo_price=299,
competitor_price=299,
)
assert diagnostics.score >= 0.76
assert diagnostics.hard_veto is False
assert diagnostics.price_basis == "manual_review"
assert diagnostics.alert_tier == "identity_review"
assert "variant_selection_review" in diagnostics.reasons
assert "focused_exact_identity_recipe_box_peelable_child_polish_catalog" in diagnostics.reasons
def test_marketplace_matcher_keeps_aroma_and_nail_variant_gaps_blocked():
from services.marketplace_product_matcher import score_marketplace_match
pavaruni_cross_type = score_marketplace_match(
"【Pavaruni】美國香氛蠟燭20種香味500g(大豆蠟燭禮盒/天然植物精油/花香/木質香/果香)",
"【美國Pavaruni】天然植物香氛精油40種香味10ml 多款任選",
)
dashing_cross_style = score_marketplace_match(
"【DASHING DIVA】MAGICPRESS 時尚潮流美甲片-輕躍裸粉(貓眼 漸層)",
"Dashing Diva/F 時尚潮流美甲片-流光裸色 30片/盒 MDF5F015CG",
)
assert pavaruni_cross_type.hard_veto is True
assert pavaruni_cross_type.score < 0.76
assert dashing_cross_style.score < 0.76
assert "variant_descriptor_conflict" in dashing_cross_style.reasons
def test_marketplace_matcher_promotes_eaoron_classic_tone_up_cream_exact_line():
from services.marketplace_product_matcher import score_marketplace_match