This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.575 拆分 PChome 型錄可比覆核 lane:`catalog_comparable` 不再只是一個總數,正式拆成 `catalog_variant_review`(選項/色號/款式待核)、`catalog_unit_review`(入數/檔期/商業條件待核)與 `catalog_identity_review`(身份採用待核)。Coverage、review queue filter、Dashboard 分段、decision envelope、Webcrumbs host data 都共用同一套 SQL helper 與 metadata;仍維持 HITL、不自動寫正式價差,讓營運可批次清理最有機會轉成單位價或正式身份的候選。
|
||||
- V10.574 接上 PChome 型錄/任選可比覆核隊列:沿用 V10.572 的 `catalog_comparable_count` 安全口徑,將高分、無 hard veto、具同品線身份證據但仍有任選/型錄/商業條件待確認的 `true_low_confidence` 候選,拆成獨立 `catalog_comparable` 篩選與 decision envelope。此隊列仍維持 HITL,不寫入正式 `competitor_prices`、不算 exact matched,並把「型錄可比」與真正「證據不足」分開,讓營運可以先批次處理最有機會轉成單位價或正式身份的候選。
|
||||
- V10.574 新增市場情報 Source Governance → Fetch Target bridge:新增 `/api/market_intel/mcp_fetch_target_source_governance_review`、市場情報頁 bridge panel 與 deployment readiness smoke target,交叉審核 Professional Source Governance 與 MCP Fetch Target Review,要求每個 target `platform_code/source_key` 都能對上已通過治理的公開 source contract;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不執行 CLI、不掛 scheduler。
|
||||
- V10.572 新增 PChome 決策支援覆蓋率:不放寬 `matched` / `decision_ready` 的 exact identity 門檻,另外把高分、無 hard veto、具同品線與規格證據,但因「任選 / 色號 / 型錄 / 即期」仍需覆核的候選,納入 `catalog_comparable_count` 與 `decision_support_rate`。Dashboard、當日業績、成長分析與 backfill 狀態摘要同步顯示「決策支援覆蓋率 / 精準可告警覆蓋 / 型錄可比 / 單位價」,讓覆蓋率提升建立在可解釋情報分層上,而不是把非 exact 商品硬寫成正式同款。
|
||||
|
||||
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.574"
|
||||
SYSTEM_VERSION = "V10.575"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
- PChome 覆核隊列本身也必須輸出 `decision_envelope`:`fetch_competitor_review_queue()`、`fetch_competitor_review_queue_page()` 與 `/api/pchome-review/queue` 的每筆候選需帶相同的 `subject`、`evidence`、`recommended_action`、`expected_impact` 與 `guardrails`,供 Dashboard、Agent、Telegram 與 PPT 共用;任何下游不得另寫一套比價狀態翻譯或繞過 HITL guardrails。
|
||||
- Dashboard 覆核卡與 `/api/export/excel/pchome-review` 也必須顯示/匯出 `decision_envelope` 的等級、資料品質、建議代碼、HITL、trace 與 `can_auto_execute=false` 邊界;操作員離開系統畫面或下載 Excel 後,仍要看得到「不可自動寫正式價差」的 guardrails。
|
||||
- OpenClaw 週報/日報/月報與 competitor PPT 不得再各自重算或翻譯 PChome 覆核狀態;必須透過 `competitor_intel_repository.summarize_review_decision_envelopes()` 讀取同一份 `decision_envelope` 摘要,並在 prompt / data_summary / KPI slide 保留 HITL 與 `can_auto_execute=false` 邊界。
|
||||
- Webcrumbs / Shared UI host data 也必須透過 `summarize_review_decision_envelopes()` 輸出 `reviewDecisionBrief`,並在 metadata 保留 review queue、HITL 與 auto-execute-blocked 數量;不得另寫一套 PChome 覆核摘要或在前端 runtime 重新推論價格行動。
|
||||
- Webcrumbs / Shared UI host data 也必須透過 `summarize_review_decision_envelopes()` 輸出 `reviewDecisionBrief`,並在 metadata 保留 review queue、HITL、auto-execute-blocked、`decision_support_rate`、`catalog_comparable_count` 與 catalog review lane counts;不得另寫一套 PChome 覆核摘要或在前端 runtime 重新推論價格行動。
|
||||
- ElephantAlpha 的 `resource_optimization` 與低信心 `ea_escalation` 也必須輸出 `decision_envelope`:資源壓力信封只能使用 `action_plans`、CPU 實測、hygiene 結果與 insight/action trace,不得加入 LLM 預測效益;`triaged_alert()` 對 `ea_escalation` 亦需渲染信封並以 `decision_id` 作為 callback 追蹤 ID。
|
||||
|
||||
## 一、四 AI Agent 路由架構
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
- 2026-05-31 起,`V10.506` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval gate:在 review decision 通過後只審核 operator human approval 摘要,要求 decision linkage、approval identity、target table、row count、dedupe keys、`approved_for_writer_preflight` approval result、decision/approval evidence refs、artifact paths、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 approval record、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到後續 writer preflight 設計。
|
||||
- 2026-05-31 起,`V10.509` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval Writer Preflight gate:在 human approval 通過後只審核 operator writer preflight 摘要,要求 approval linkage、writer_preflight_id、target operation、row count、dedupe keys、approved decision 到 target review_state 的逐列映射、decision/approval/preflight evidence refs、matched row exact-identity/variant/overwrite guard 與 operator boundary;仍不讀 token、不執行 CLI、不開 DB、不寫 preflight/approval/decision/match、不更新 review_state、不補 queue、不掛 scheduler,只放行到後續 CLI review / run package 設計。
|
||||
- 2026-06-01 起,`V10.566` 新增市場情報 Professional Source Governance gate:將 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 納入 source contract,並接上 `/api/market_intel/mcp_professional_source_governance`、UI preview panel、deployment readiness check 與 production smoke target;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler。
|
||||
- 2026-06-04 起,`V10.575` 拆分 PChome 型錄可比覆核 lane:`catalog_comparable` 會依 diagnostic evidence 分成選項/色號、單位/入數與身份採用三條人工處理路徑,Dashboard、decision envelope、coverage 與 Webcrumbs host data 使用同一套統計與 HITL guardrail。
|
||||
- 2026-06-03 起,`V10.574` 新增市場情報 Source Governance → Fetch Target bridge:`/api/market_intel/mcp_fetch_target_source_governance_review` 交叉審核 Professional Source Governance 與 MCP Fetch Target Review,要求 target `platform_code/source_key` 全部命中已治理 source contract;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不執行 CLI、不掛 scheduler,只放行到後續人工 fetch run package review。
|
||||
- 2026-06-02 起,`V10.567` 將 MCP 市場洞察 fallback 收斂為 GCP-A / GCP-B only,不再讓 111 承接非即時市場分析長任務;預設 timeout 25 秒、`num_predict` 500,GCP 不可用時直接保守降級,避免 Elephant Alpha 60 秒 timeout 與 111 負載尖峰。
|
||||
- 2026-06-02 起,`V10.568` 將價格類 `decision_envelope` 的 Telegram 直送訊息改為專業 brief:標的、價格證據、比對證據、人工下一步四段式;review queue 信封 subject 同步帶 `momo_price` / `competitor_price`,讓 Telegram、PPT、Webcrumbs 與 AI 摘要共用價格證據。
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-06-01:PChome 比價新鮮度操作閉環
|
||||
- **V10.575 PChome 型錄可比覆核 lane 分流**: `catalog_comparable` 進一步拆成 `catalog_variant_review`、`catalog_unit_review` 與 `catalog_identity_review`。Coverage SQL、review queue filter、Dashboard 分段、decision envelope 與 Webcrumbs host data 共用同一套 helper,將選項/色號/款式、入數/商業條件、身份採用三種人工閉環路徑分開統計與瀏覽;仍維持 HITL,不自動寫正式價差。
|
||||
- **V10.574 PChome 型錄/任選可比覆核隊列**: 將 V10.572 的 `catalog_comparable_count` 派生口徑正式接進 PChome review queue。高分、無 hard veto、具同品線身份證據但仍有任選/型錄/商業條件待確認的 `true_low_confidence` 會進獨立 `catalog_comparable` 篩選、狀態標籤與 decision envelope;真正 `true_low_confidence` 會排除這批候選,避免重複出現在「證據不足」。此變更不放寬 `MIN_MATCH_SCORE`、不寫正式 `competitor_prices`、不算 exact matched,只把最有機會人工批次確認的候選變成可操作隊列。
|
||||
- **V10.574 市場情報 Source Governance → Fetch Target bridge**: 新增 `/api/market_intel/mcp_fetch_target_source_governance_review`、preview service 與市場情報頁 bridge panel,交叉審核 Professional Source Governance 與 MCP Fetch Target Review。此 gate 要求每個 target `platform_code/source_key` 都能對上已通過治理的公開 source contract,並同步納入 deployment readiness preview-safe check 與 production smoke target;API/UI 仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不執行 CLI、不掛 scheduler。
|
||||
- **V10.572 PChome 決策支援覆蓋率分層**: 覆蓋率不再只有 exact `decision_ready_rate`。`fetch_competitor_coverage()` cache 升到 v11,新增 `catalog_comparable_count`、`decision_support_count`、`decision_support_rate` 與非 exact 支援數;只納入高分、無 hard veto、同時具型錄/任選/商業條件訊號與強身份證據,且排除品類、品線、入數、香味、型號、價格極端等硬衝突的候選。Dashboard、daily、growth 與 backfill JS 同步顯示「決策支援覆蓋率 / 精準可告警覆蓋 / 型錄可比 / 單位價」,提升可用情報覆蓋但不污染正式 `matched`。
|
||||
|
||||
@@ -69,6 +69,9 @@ REVIEW_STATUS_OPTIONS = [
|
||||
'statuses': ('unit_comparable', 'refresh_unit_comparable'),
|
||||
},
|
||||
{'key': 'catalog_comparable', 'label': '型錄可比', 'statuses': ('true_low_confidence',)},
|
||||
{'key': 'catalog_variant_review', 'label': '選項待核', 'statuses': ('true_low_confidence',)},
|
||||
{'key': 'catalog_unit_review', 'label': '入數待核', 'statuses': ('true_low_confidence',)},
|
||||
{'key': 'catalog_identity_review', 'label': '身份待核', 'statuses': ('true_low_confidence',)},
|
||||
{'key': 'identity_veto', 'label': '已排除', 'statuses': ('identity_veto',)},
|
||||
{'key': 'recoverable_low_score', 'label': '近門檻可救', 'statuses': ('recoverable_low_score',)},
|
||||
{'key': 'true_low_confidence', 'label': '證據不足', 'statuses': ('true_low_confidence',)},
|
||||
@@ -694,6 +697,12 @@ def _merge_competitor_review_context(overview, review_context):
|
||||
for option in REVIEW_STATUS_OPTIONS:
|
||||
if option['key'] == 'catalog_comparable':
|
||||
review_status_counts[option['key']] = int(coverage.get('catalog_comparable_count') or 0)
|
||||
elif option['key'] == 'catalog_variant_review':
|
||||
review_status_counts[option['key']] = int(coverage.get('catalog_variant_review_count') or 0)
|
||||
elif option['key'] == 'catalog_unit_review':
|
||||
review_status_counts[option['key']] = int(coverage.get('catalog_unit_review_count') or 0)
|
||||
elif option['key'] == 'catalog_identity_review':
|
||||
review_status_counts[option['key']] = int(coverage.get('catalog_identity_review_count') or 0)
|
||||
elif option['key'] == 'true_low_confidence':
|
||||
review_status_counts[option['key']] = max(
|
||||
int(attempt_status.get('true_low_confidence') or 0)
|
||||
@@ -728,6 +737,10 @@ def _merge_competitor_review_context(overview, review_context):
|
||||
'decision_support_non_exact_count': int(coverage.get('decision_support_non_exact_count') or 0),
|
||||
'catalog_comparable_count': int(coverage.get('catalog_comparable_count') or 0),
|
||||
'catalog_comparable_rate': coverage.get('catalog_comparable_rate', 0),
|
||||
'catalog_variant_review_count': int(coverage.get('catalog_variant_review_count') or 0),
|
||||
'catalog_unit_review_count': int(coverage.get('catalog_unit_review_count') or 0),
|
||||
'catalog_identity_review_count': int(coverage.get('catalog_identity_review_count') or 0),
|
||||
'catalog_review_plan': coverage.get('catalog_review_plan') or {},
|
||||
'stale_match_count': int(coverage.get('stale_matches') or 0),
|
||||
'unknown_freshness_count': int(coverage.get('unknown_freshness_matches') or 0),
|
||||
'pending_match_count': int(coverage.get('pending') or overview.get('pending_match_count') or 0),
|
||||
@@ -883,6 +896,10 @@ def _load_competitor_decision_overview(session, latest_items=None):
|
||||
'decision_support_non_exact_count': 0,
|
||||
'catalog_comparable_count': 0,
|
||||
'catalog_comparable_rate': 0,
|
||||
'catalog_variant_review_count': 0,
|
||||
'catalog_unit_review_count': 0,
|
||||
'catalog_identity_review_count': 0,
|
||||
'catalog_review_plan': {},
|
||||
'stale_match_count': 0,
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
@@ -1748,6 +1765,10 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
'decision_support_non_exact_count': 0,
|
||||
'catalog_comparable_count': 0,
|
||||
'catalog_comparable_rate': 0,
|
||||
'catalog_variant_review_count': 0,
|
||||
'catalog_unit_review_count': 0,
|
||||
'catalog_identity_review_count': 0,
|
||||
'catalog_review_plan': {},
|
||||
'unknown_freshness_count': 0,
|
||||
'pchome_advantage_count': 0,
|
||||
'momo_threat_count': 0,
|
||||
@@ -1782,6 +1803,10 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
overview.setdefault('decision_support_non_exact_count', 0)
|
||||
overview.setdefault('catalog_comparable_count', 0)
|
||||
overview.setdefault('catalog_comparable_rate', 0)
|
||||
overview.setdefault('catalog_variant_review_count', 0)
|
||||
overview.setdefault('catalog_unit_review_count', 0)
|
||||
overview.setdefault('catalog_identity_review_count', 0)
|
||||
overview.setdefault('catalog_review_plan', {})
|
||||
return overview
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,14 @@ CATALOG_COMPARABLE_SIGNAL_REASONS = {
|
||||
"commercial_condition_gap",
|
||||
"catalog_count_omission",
|
||||
}
|
||||
CATALOG_VARIANT_REVIEW_REASONS = {
|
||||
"variant_selection_review",
|
||||
"makeup_catalog_selection_gap",
|
||||
}
|
||||
CATALOG_UNIT_REVIEW_REASONS = {
|
||||
"commercial_condition_gap",
|
||||
"catalog_count_omission",
|
||||
}
|
||||
CATALOG_COMPARABLE_IDENTITY_REASONS = {
|
||||
"strong_product_line_match",
|
||||
"strong_exact_spec_match",
|
||||
@@ -88,6 +96,9 @@ REVIEW_STATUS_FILTER_GROUPS = {
|
||||
"rescore_accepted": ("rescore_accepted_current",),
|
||||
"unit_comparable": ("unit_comparable", "refresh_unit_comparable"),
|
||||
"catalog_comparable": ("true_low_confidence",),
|
||||
"catalog_variant_review": ("true_low_confidence",),
|
||||
"catalog_unit_review": ("true_low_confidence",),
|
||||
"catalog_identity_review": ("true_low_confidence",),
|
||||
"identity_veto": ("identity_veto",),
|
||||
"low_score": ("low_score", "refresh_low_score", "recoverable_low_score", "true_low_confidence"),
|
||||
"recoverable_low_score": ("recoverable_low_score",),
|
||||
@@ -152,6 +163,21 @@ DECISION_ACTION_LABELS = {
|
||||
"refresh_or_compare_identity": "刷新價格或比較候選",
|
||||
"human_review": "人工覆核",
|
||||
}
|
||||
CATALOG_REVIEW_LANE_LABELS = {
|
||||
"catalog_variant_review": "選項 / 色號待核",
|
||||
"catalog_unit_review": "單位價 / 入數待核",
|
||||
"catalog_identity_review": "身份採用待核",
|
||||
}
|
||||
CATALOG_REVIEW_LANE_ACTION_HINTS = {
|
||||
"catalog_variant_review": "先確認 MOMO 選項、色號、香味或款式是否涵蓋 PChome 候選;一致才採用,無法對齊就補搜尋或否決",
|
||||
"catalog_unit_review": "先確認入數、贈品、檔期或商業條件;總價不可直比時標記單位價",
|
||||
"catalog_identity_review": "身份證據已完整但仍留在 HITL;人工確認後可採用同款",
|
||||
}
|
||||
CATALOG_REVIEW_LANE_PRIMARY_ACTIONS = {
|
||||
"catalog_variant_review": "needs_research",
|
||||
"catalog_unit_review": "unit_price_required",
|
||||
"catalog_identity_review": "accept_identity",
|
||||
}
|
||||
DATA_QUALITY_LABELS = {
|
||||
"complete": "證據完整",
|
||||
"partial": "證據部分完整",
|
||||
@@ -322,6 +348,29 @@ def _catalog_comparable_sql(alias: str = "la") -> str:
|
||||
)"""
|
||||
|
||||
|
||||
def _catalog_review_lane_sql(alias: str = "la", lane: str = "catalog_comparable") -> str:
|
||||
diagnostic_codes = f"{alias}.diagnostic_codes"
|
||||
catalog_sql = _catalog_comparable_sql(alias)
|
||||
variant_sql = _jsonb_any_array_predicate(diagnostic_codes, CATALOG_VARIANT_REVIEW_REASONS)
|
||||
unit_sql = _jsonb_any_array_predicate(diagnostic_codes, CATALOG_UNIT_REVIEW_REASONS)
|
||||
if lane == "catalog_variant_review":
|
||||
return f"({catalog_sql} AND {variant_sql})"
|
||||
if lane == "catalog_unit_review":
|
||||
return f"({catalog_sql} AND NOT {variant_sql} AND {unit_sql})"
|
||||
if lane == "catalog_identity_review":
|
||||
return f"({catalog_sql} AND NOT {variant_sql} AND NOT {unit_sql})"
|
||||
return catalog_sql
|
||||
|
||||
|
||||
def _catalog_review_lane_case_sql(alias: str = "la") -> str:
|
||||
return f"""CASE
|
||||
WHEN {_catalog_review_lane_sql(alias, "catalog_variant_review")} THEN 'catalog_variant_review'
|
||||
WHEN {_catalog_review_lane_sql(alias, "catalog_unit_review")} THEN 'catalog_unit_review'
|
||||
WHEN {_catalog_review_lane_sql(alias, "catalog_identity_review")} THEN 'catalog_identity_review'
|
||||
ELSE ''
|
||||
END"""
|
||||
|
||||
|
||||
def _tag_suffix(tags: list[str], prefix: str) -> str:
|
||||
marker = f"{prefix}_"
|
||||
for tag in tags:
|
||||
@@ -495,6 +544,43 @@ def _build_review_difference_highlights(
|
||||
return highlights
|
||||
|
||||
|
||||
def _build_catalog_review_guidance(
|
||||
catalog_comparable: bool,
|
||||
catalog_review_lane: str,
|
||||
diagnostic_reasons: list[dict[str, str]],
|
||||
) -> dict[str, Any]:
|
||||
if not catalog_comparable:
|
||||
return {}
|
||||
reason_codes = {
|
||||
str(reason.get("code") or "")
|
||||
for reason in diagnostic_reasons or []
|
||||
if reason.get("code")
|
||||
}
|
||||
if catalog_review_lane in CATALOG_REVIEW_LANE_LABELS:
|
||||
lane = catalog_review_lane
|
||||
elif reason_codes & CATALOG_VARIANT_REVIEW_REASONS:
|
||||
lane = "catalog_variant_review"
|
||||
elif reason_codes & CATALOG_UNIT_REVIEW_REASONS:
|
||||
lane = "catalog_unit_review"
|
||||
else:
|
||||
lane = "catalog_identity_review"
|
||||
reason_labels = [
|
||||
str(reason.get("label") or reason.get("code") or "")
|
||||
for reason in diagnostic_reasons or []
|
||||
if reason.get("label") or reason.get("code")
|
||||
]
|
||||
primary_action = CATALOG_REVIEW_LANE_PRIMARY_ACTIONS.get(lane, "accept_identity")
|
||||
return {
|
||||
"lane": lane,
|
||||
"lane_label": CATALOG_REVIEW_LANE_LABELS.get(lane, "型錄可比待核"),
|
||||
"action_hint": CATALOG_REVIEW_LANE_ACTION_HINTS.get(lane, "人工確認型錄條件後再決定採用或否決"),
|
||||
"primary_review_action": primary_action,
|
||||
"primary_review_action_label": MANUAL_REVIEW_ACTION_LABELS.get(primary_action, "人工覆核"),
|
||||
"reason_labels": reason_labels[:5],
|
||||
"can_auto_execute": False,
|
||||
}
|
||||
|
||||
|
||||
def _build_unit_comparison_for_attempt(row: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
status = str(row.get("attempt_status") or "")
|
||||
if status not in UNIT_PRICE_DECISION_STATUSES:
|
||||
@@ -633,11 +719,20 @@ def _parse_existing_match_conflict(error_message: Any) -> dict[str, Any]:
|
||||
def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build the shared evidence contract for an operator review queue item."""
|
||||
attempt_status = str(item.get("attempt_status") or "")
|
||||
action_code = (
|
||||
"review_catalog_comparable"
|
||||
if item.get("catalog_comparable")
|
||||
else _review_action_code(attempt_status)
|
||||
catalog_guidance = (
|
||||
item.get("catalog_review_guidance")
|
||||
if isinstance(item.get("catalog_review_guidance"), dict)
|
||||
else {}
|
||||
)
|
||||
action_code = _review_action_code(attempt_status)
|
||||
if item.get("catalog_comparable"):
|
||||
primary_action = str(catalog_guidance.get("primary_review_action") or "")
|
||||
if primary_action == "accept_identity":
|
||||
action_code = "review_accept_identity"
|
||||
elif primary_action in {"needs_research", "unit_price_required"}:
|
||||
action_code = primary_action
|
||||
else:
|
||||
action_code = "review_catalog_comparable"
|
||||
momo_price = _num(item.get("momo_price"))
|
||||
candidate_price = _num(item.get("candidate_pc_price"))
|
||||
gap_amount = None
|
||||
@@ -680,8 +775,12 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
evidence.append({
|
||||
"type": "review_bucket",
|
||||
"metric": "catalog_comparable",
|
||||
"value": "true",
|
||||
"basis": "true_low_confidence + high score + identity anchor + catalog/variant review signal + no hard veto",
|
||||
"value": item.get("catalog_review_lane") or catalog_guidance.get("lane") or "catalog_comparable",
|
||||
"basis": (
|
||||
catalog_guidance.get("lane_label")
|
||||
if catalog_guidance.get("lane_label")
|
||||
else "true_low_confidence + high score + identity anchor + catalog/variant review signal + no hard veto"
|
||||
),
|
||||
})
|
||||
identity_evidence = item.get("identity_evidence")
|
||||
identity_summary = _build_identity_evidence_summary(identity_evidence)
|
||||
@@ -776,6 +875,7 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"candidate_gap_pct": gap_pct,
|
||||
"unit_price_insight": unit_price_insight if isinstance(unit_price_insight, dict) else {},
|
||||
"existing_match_conflict": existing_conflict if isinstance(existing_conflict, dict) else {},
|
||||
"catalog_review_guidance": catalog_guidance,
|
||||
"risk_reduction": "medium" if attempt_status in {"rescore_accepted_current", "recoverable_low_score"} else "watch",
|
||||
},
|
||||
"confidence": round(_num(item.get("best_match_score")), 3),
|
||||
@@ -794,6 +894,7 @@ def _build_review_decision_envelope(item: dict[str, Any]) -> dict[str, Any]:
|
||||
else ""
|
||||
),
|
||||
"catalog_comparable": bool(item.get("catalog_comparable")),
|
||||
"catalog_review_lane": item.get("catalog_review_lane") or catalog_guidance.get("lane") or "",
|
||||
"price_is_identity_evidence": False,
|
||||
},
|
||||
"trace": {
|
||||
@@ -830,6 +931,7 @@ def summarize_review_decision_envelopes(
|
||||
guardrails = envelope.get("guardrails") if isinstance(envelope.get("guardrails"), dict) else {}
|
||||
action = envelope.get("recommended_action") if isinstance(envelope.get("recommended_action"), dict) else {}
|
||||
expected = envelope.get("expected_impact") if isinstance(envelope.get("expected_impact"), dict) else {}
|
||||
catalog_guidance = expected.get("catalog_review_guidance") if isinstance(expected.get("catalog_review_guidance"), dict) else {}
|
||||
evidence = envelope.get("evidence") if isinstance(envelope.get("evidence"), list) else []
|
||||
|
||||
severity = str(envelope.get("severity") or "P4")
|
||||
@@ -888,6 +990,8 @@ def summarize_review_decision_envelopes(
|
||||
line_parts.append(unit_text)
|
||||
if diff_text:
|
||||
line_parts.append(diff_text)
|
||||
if catalog_guidance.get("lane_label"):
|
||||
line_parts.append(str(catalog_guidance.get("lane_label")))
|
||||
if evidence_basis:
|
||||
line_parts.append(evidence_basis)
|
||||
line = " | ".join(part for part in line_parts if part)
|
||||
@@ -907,6 +1011,7 @@ def summarize_review_decision_envelopes(
|
||||
"candidate_gap_pct": gap_pct,
|
||||
"unit_price_gap_pct": unit_gap_pct,
|
||||
"difference_highlights": difference_highlights if isinstance(difference_highlights, list) else [],
|
||||
"catalog_review_guidance": catalog_guidance,
|
||||
"line": line,
|
||||
})
|
||||
|
||||
@@ -946,13 +1051,24 @@ def _format_competitor_review_item(row: dict[str, Any]) -> dict[str, Any]:
|
||||
difference_highlights = _build_review_difference_highlights(diagnostic_reasons, identity_evidence)
|
||||
existing_match_conflict = _parse_existing_match_conflict(match_diagnostic)
|
||||
catalog_comparable = bool(item.get("catalog_comparable"))
|
||||
catalog_review_lane = str(item.get("catalog_review_lane") or "")
|
||||
catalog_review_guidance = _build_catalog_review_guidance(
|
||||
catalog_comparable,
|
||||
catalog_review_lane,
|
||||
diagnostic_reasons,
|
||||
)
|
||||
resolved_catalog_review_lane = (
|
||||
str(catalog_review_guidance.get("lane") or "")
|
||||
if catalog_comparable
|
||||
else catalog_review_lane
|
||||
)
|
||||
status_label = _attempt_status_label(item.get("attempt_status"))
|
||||
action_label = _attempt_action_label(item.get("attempt_status"))
|
||||
review_bucket = str(item.get("attempt_status") or "")
|
||||
if catalog_comparable:
|
||||
status_label = "型錄/任選可比"
|
||||
action_label = "人工確認型錄、任選與規格條件後,再轉單位價或採用身份"
|
||||
review_bucket = "catalog_comparable"
|
||||
status_label = catalog_review_guidance.get("lane_label") or "型錄/任選可比"
|
||||
action_label = catalog_review_guidance.get("action_hint") or "人工確認型錄、任選與規格條件後,再轉單位價或採用身份"
|
||||
review_bucket = catalog_review_guidance.get("lane") or "catalog_comparable"
|
||||
formatted = {
|
||||
"sku": str(item.get("sku") or ""),
|
||||
"name": item.get("name") or "",
|
||||
@@ -963,6 +1079,8 @@ def _format_competitor_review_item(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"status_label": status_label,
|
||||
"action_label": action_label,
|
||||
"catalog_comparable": catalog_comparable,
|
||||
"catalog_review_lane": resolved_catalog_review_lane,
|
||||
"catalog_review_guidance": catalog_review_guidance,
|
||||
"candidate_count": int(item.get("candidate_count") or 0),
|
||||
"candidate_pc_id": item.get("best_competitor_product_id"),
|
||||
"candidate_pc_name": item.get("best_competitor_product_name") or "",
|
||||
@@ -1060,7 +1178,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE
|
||||
|
||||
def fetch_competitor_coverage(engine) -> dict:
|
||||
return _cached_payload(
|
||||
f"coverage:v12:floor={PCHOME_MATCH_SCORE_FLOOR}:catalog_floor={CATALOG_COMPARABLE_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1:unknown_freshness=1:decision_support=1",
|
||||
f"coverage:v13:floor={PCHOME_MATCH_SCORE_FLOOR}:catalog_floor={CATALOG_COMPARABLE_SCORE_FLOOR}:manual_reviews=1:rescore=1:review_no_fresh=1:decision_ready=1:open_queue=1:unknown_freshness=1:decision_support=1:catalog_plan=1",
|
||||
lambda: _fetch_competitor_coverage_uncached(engine),
|
||||
)
|
||||
|
||||
@@ -1085,6 +1203,15 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"decision_support_non_exact_count": 0,
|
||||
"catalog_comparable_count": 0,
|
||||
"catalog_comparable_rate": 0,
|
||||
"catalog_variant_review_count": 0,
|
||||
"catalog_unit_review_count": 0,
|
||||
"catalog_identity_review_count": 0,
|
||||
"catalog_review_plan": {
|
||||
"variant_review": 0,
|
||||
"unit_review": 0,
|
||||
"identity_review": 0,
|
||||
"total": 0,
|
||||
},
|
||||
"identity_coverage_matches": 0,
|
||||
"identity_coverage_rate": 0,
|
||||
"pending_identity_count": 0,
|
||||
@@ -1209,6 +1336,27 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
WHERE fc.sku IS NULL
|
||||
AND {_catalog_comparable_sql("la")}
|
||||
) AS catalog_comparable_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku
|
||||
JOIN latest_attempt la ON la.sku = lm.sku
|
||||
WHERE fc.sku IS NULL
|
||||
AND {_catalog_review_lane_sql("la", "catalog_variant_review")}
|
||||
) AS catalog_variant_review_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku
|
||||
JOIN latest_attempt la ON la.sku = lm.sku
|
||||
WHERE fc.sku IS NULL
|
||||
AND {_catalog_review_lane_sql("la", "catalog_unit_review")}
|
||||
) AS catalog_unit_review_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM latest_momo lm
|
||||
LEFT JOIN fresh_competitor fc ON fc.sku = lm.sku
|
||||
JOIN latest_attempt la ON la.sku = lm.sku
|
||||
WHERE fc.sku IS NULL
|
||||
AND {_catalog_review_lane_sql("la", "catalog_identity_review")}
|
||||
) AS catalog_identity_review_count,
|
||||
COALESCE(la.attempt_status, 'never_attempted') AS attempt_status,
|
||||
COUNT(*) AS status_count
|
||||
FROM latest_momo lm
|
||||
@@ -1232,6 +1380,9 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
}
|
||||
unit_count = sum(statuses.get(status, 0) for status in UNIT_COMPARABLE_STATUSES)
|
||||
catalog_comparable_count = int(rows[0].get("catalog_comparable_count") or 0) if rows else 0
|
||||
catalog_variant_review_count = int(rows[0].get("catalog_variant_review_count") or 0) if rows else 0
|
||||
catalog_unit_review_count = int(rows[0].get("catalog_unit_review_count") or 0) if rows else 0
|
||||
catalog_identity_review_count = int(rows[0].get("catalog_identity_review_count") or 0) if rows else 0
|
||||
decision_support_non_exact_count = unit_count + catalog_comparable_count
|
||||
decision_support_count = fresh + decision_support_non_exact_count
|
||||
rescore_accepted_count = int(statuses.get("rescore_accepted_current") or 0)
|
||||
@@ -1251,6 +1402,15 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"decision_support_non_exact_count": decision_support_non_exact_count,
|
||||
"catalog_comparable_count": catalog_comparable_count,
|
||||
"catalog_comparable_rate": round(catalog_comparable_count / max(active, 1) * 100, 1),
|
||||
"catalog_variant_review_count": catalog_variant_review_count,
|
||||
"catalog_unit_review_count": catalog_unit_review_count,
|
||||
"catalog_identity_review_count": catalog_identity_review_count,
|
||||
"catalog_review_plan": {
|
||||
"variant_review": catalog_variant_review_count,
|
||||
"unit_review": catalog_unit_review_count,
|
||||
"identity_review": catalog_identity_review_count,
|
||||
"total": catalog_comparable_count,
|
||||
},
|
||||
"identity_coverage_matches": valid,
|
||||
"identity_coverage_rate": round(valid / max(active, 1) * 100, 1),
|
||||
"pending_identity_count": pending,
|
||||
@@ -1534,7 +1694,7 @@ def fetch_competitor_review_queue(engine, limit: int = 12) -> list[dict]:
|
||||
"""可行動的 PChome 比對覆核隊列,供 Dashboard / AI / PPT 共用。"""
|
||||
limit = max(1, min(int(limit or 12), 50))
|
||||
return _cached_payload(
|
||||
f"review_queue:v4:limit={limit}:floor={PCHOME_MATCH_SCORE_FLOOR}:catalog=1",
|
||||
f"review_queue:v5:limit={limit}:floor={PCHOME_MATCH_SCORE_FLOOR}:catalog=1:lanes=1",
|
||||
lambda: _fetch_competitor_review_queue_uncached(engine, limit=limit),
|
||||
)
|
||||
|
||||
@@ -1557,7 +1717,7 @@ def fetch_competitor_review_queue_page(
|
||||
if status_filter not in REVIEW_STATUS_FILTER_GROUPS:
|
||||
status_filter = ""
|
||||
cache_key = (
|
||||
"review_queue_page:v4:"
|
||||
"review_queue_page:v5:"
|
||||
f"page={page}:per={per_page}:q={search_query.lower()}:cat={category}:"
|
||||
f"status={status_filter}:"
|
||||
f"count={int(bool(count_total))}:"
|
||||
@@ -1588,6 +1748,7 @@ def _review_queue_cte_and_filter(
|
||||
status_values = REVIEW_STATUS_FILTER_GROUPS.get(status_filter) or tuple(ACTIONABLE_ATTEMPT_STATUSES)
|
||||
status_sql = ", ".join(f"'{status}'" for status in status_values)
|
||||
catalog_comparable_expr = _catalog_comparable_sql("la")
|
||||
catalog_lane_expr = _catalog_review_lane_case_sql("la")
|
||||
filters = [
|
||||
f"la.attempt_status IN ({status_sql})",
|
||||
f"""NOT EXISTS (
|
||||
@@ -1604,6 +1765,12 @@ def _review_queue_cte_and_filter(
|
||||
]
|
||||
if status_filter == "catalog_comparable":
|
||||
filters.append(catalog_comparable_expr)
|
||||
elif status_filter in {
|
||||
"catalog_variant_review",
|
||||
"catalog_unit_review",
|
||||
"catalog_identity_review",
|
||||
}:
|
||||
filters.append(_catalog_review_lane_sql("la", status_filter))
|
||||
elif status_filter == "true_low_confidence":
|
||||
filters.append(f"NOT {catalog_comparable_expr}")
|
||||
if search_query:
|
||||
@@ -1651,6 +1818,7 @@ def _review_queue_cte_and_filter(
|
||||
la.error_message,
|
||||
la.attempted_at,
|
||||
{catalog_comparable_expr} AS catalog_comparable,
|
||||
{catalog_lane_expr} AS catalog_review_lane,
|
||||
CASE
|
||||
WHEN la.attempt_status = 'rescore_accepted_current' THEN 0
|
||||
WHEN la.attempt_status IN ('unit_comparable', 'refresh_unit_comparable') THEN 1
|
||||
@@ -1836,7 +2004,8 @@ def _fetch_competitor_review_queue_uncached(engine, limit: int = 12) -> list[dic
|
||||
la.match_diagnostic_json,
|
||||
la.error_message,
|
||||
la.attempted_at,
|
||||
{_catalog_comparable_sql("la")} AS catalog_comparable
|
||||
{_catalog_comparable_sql("la")} AS catalog_comparable,
|
||||
{_catalog_review_lane_case_sql("la")} AS catalog_review_lane
|
||||
FROM latest_momo lm
|
||||
JOIN latest_attempt la ON la.sku = lm.sku
|
||||
LEFT JOIN valid_competitor vc ON vc.sku = lm.sku
|
||||
|
||||
@@ -90,6 +90,18 @@ def _coverage_metadata(coverage: dict, row_count: int) -> dict:
|
||||
or 0
|
||||
),
|
||||
"decision_ready_rate": _num(coverage.get("decision_ready_rate")),
|
||||
"decision_support_count": int(coverage.get("decision_support_count") or 0),
|
||||
"decision_support_rate": _num(coverage.get("decision_support_rate")),
|
||||
"decision_support_non_exact_count": int(
|
||||
coverage.get("decision_support_non_exact_count") or 0
|
||||
),
|
||||
"catalog_comparable_count": int(coverage.get("catalog_comparable_count") or 0),
|
||||
"catalog_comparable_rate": _num(coverage.get("catalog_comparable_rate")),
|
||||
"catalog_variant_review_count": int(coverage.get("catalog_variant_review_count") or 0),
|
||||
"catalog_unit_review_count": int(coverage.get("catalog_unit_review_count") or 0),
|
||||
"catalog_identity_review_count": int(coverage.get("catalog_identity_review_count") or 0),
|
||||
"catalog_review_plan": coverage.get("catalog_review_plan") or {},
|
||||
"unit_comparable_count": int(coverage.get("unit_comparable_count") or 0),
|
||||
"fresh_match_count": int(coverage.get("fresh_matches") or coverage.get("fresh_match_count") or 0),
|
||||
"fresh_match_rate": _num(coverage.get("fresh_match_rate")),
|
||||
"stale_match_count": int(coverage.get("stale_matches") or coverage.get("stale_match_count") or 0),
|
||||
|
||||
@@ -632,6 +632,11 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if review.catalog_review_guidance %}
|
||||
<div class="dashboard-review-note">
|
||||
覆核建議:{{ review.catalog_review_guidance.lane_label }} · {{ review.catalog_review_guidance.action_hint }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dashboard-ai-evidence-line">
|
||||
{% if review.candidate_count %}
|
||||
<span>{{ review.candidate_count }} 筆候選</span>
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
"def _fetch_manual_review_summary", 1
|
||||
)[0]
|
||||
|
||||
assert "coverage:v12" in source
|
||||
assert "coverage:v13" in source
|
||||
assert "CATALOG_COMPARABLE_SCORE_FLOOR" in source
|
||||
assert "rescore_accepted_count" in coverage_source
|
||||
assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source
|
||||
@@ -105,7 +105,13 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
assert "\"decision_support_rate\": round(decision_support_count / max(active, 1) * 100, 1)" in coverage_source
|
||||
assert "\"catalog_comparable_count\": catalog_comparable_count" in coverage_source
|
||||
assert "_catalog_comparable_sql(\"la\")" in coverage_source
|
||||
assert "_catalog_review_lane_sql(\"la\", \"catalog_variant_review\")" in coverage_source
|
||||
assert "_catalog_review_lane_sql(\"la\", \"catalog_unit_review\")" in coverage_source
|
||||
assert "_catalog_review_lane_sql(\"la\", \"catalog_identity_review\")" in coverage_source
|
||||
assert "\"catalog_review_plan\": {" in coverage_source
|
||||
assert "CATALOG_COMPARABLE_SIGNAL_REASONS" in source
|
||||
assert "CATALOG_VARIANT_REVIEW_REASONS" in source
|
||||
assert "CATALOG_UNIT_REVIEW_REASONS" in source
|
||||
assert "CATALOG_COMPARABLE_IDENTITY_REASONS" in source
|
||||
assert "CATALOG_COMPARABLE_BLOCK_REASONS" in source
|
||||
assert "\"identity_coverage_matches\": valid" in coverage_source
|
||||
@@ -189,6 +195,9 @@ def test_competitor_review_filters_split_low_score_operational_buckets():
|
||||
from services.competitor_intel_repository import REVIEW_STATUS_FILTER_GROUPS
|
||||
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["catalog_comparable"] == ("true_low_confidence",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["catalog_variant_review"] == ("true_low_confidence",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["catalog_unit_review"] == ("true_low_confidence",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["catalog_identity_review"] == ("true_low_confidence",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["recoverable_low_score"] == ("recoverable_low_score",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["true_low_confidence"] == ("true_low_confidence",)
|
||||
assert REVIEW_STATUS_FILTER_GROUPS["legacy_low_score"] == ("low_score", "refresh_low_score")
|
||||
@@ -225,13 +234,21 @@ def test_catalog_comparable_review_item_keeps_exact_match_guardrail():
|
||||
},
|
||||
})
|
||||
|
||||
assert item["review_bucket"] == "catalog_comparable"
|
||||
assert item["status_label"] == "型錄/任選可比"
|
||||
assert "型錄、任選" in item["action_label"]
|
||||
assert item["review_bucket"] == "catalog_variant_review"
|
||||
assert item["status_label"] == "選項 / 色號待核"
|
||||
assert item["catalog_review_guidance"]["primary_review_action"] == "needs_research"
|
||||
assert "選項、色號" in item["action_label"]
|
||||
envelope = item["decision_envelope"]
|
||||
assert envelope["recommended_action"]["action"] == "review_catalog_comparable"
|
||||
assert envelope["recommended_action"]["action"] == "needs_research"
|
||||
assert envelope["guardrails"]["can_auto_execute"] is False
|
||||
assert envelope["guardrails"]["catalog_comparable"] is True
|
||||
assert envelope["guardrails"]["catalog_review_lane"] == "catalog_variant_review"
|
||||
assert envelope["expected_impact"]["catalog_review_guidance"]["lane"] == "catalog_variant_review"
|
||||
assert any(
|
||||
evidence["metric"] == "catalog_comparable"
|
||||
and evidence["value"] == "catalog_variant_review"
|
||||
for evidence in envelope["evidence"]
|
||||
)
|
||||
assert any(evidence["metric"] == "catalog_comparable" for evidence in envelope["evidence"])
|
||||
|
||||
|
||||
|
||||
@@ -72,8 +72,13 @@ def test_competitor_review_queue_starts_from_latest_attempts_not_all_products():
|
||||
assert "cma.hard_veto" in review_cte_body
|
||||
assert "cma.diagnostic_codes" in review_cte_body
|
||||
assert "catalog_comparable" in review_cte_body
|
||||
assert "catalog_review_lane" in review_cte_body
|
||||
assert "_catalog_comparable_sql(\"la\")" in review_cte_body
|
||||
assert "_catalog_review_lane_case_sql(\"la\")" in review_cte_body
|
||||
assert "status_filter == \"catalog_comparable\"" in review_cte_body
|
||||
assert "\"catalog_variant_review\"" in review_cte_body
|
||||
assert "\"catalog_unit_review\"" in review_cte_body
|
||||
assert "\"catalog_identity_review\"" in review_cte_body
|
||||
assert "status_filter == \"true_low_confidence\"" in review_cte_body
|
||||
assert "FROM latest_momo lm" not in review_cte_body
|
||||
assert "valid_competitor AS" not in review_cte_body
|
||||
|
||||
@@ -164,6 +164,12 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "catalog_comparable_count" in route_source
|
||||
assert "'catalog_comparable'" in route_source
|
||||
assert "型錄可比" in route_source
|
||||
assert "'catalog_variant_review'" in route_source
|
||||
assert "選項待核" in route_source
|
||||
assert "'catalog_unit_review'" in route_source
|
||||
assert "入數待核" in route_source
|
||||
assert "'catalog_identity_review'" in route_source
|
||||
assert "身份待核" in route_source
|
||||
assert "rescore_accepted_count" in route_source
|
||||
assert "filter_type == 'pchome_review'" in route_source
|
||||
assert "total_items = review_queue_total" in route_source
|
||||
@@ -211,6 +217,8 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "否決候選" in dashboard
|
||||
assert "標記單位價" in dashboard
|
||||
assert "補搜尋" in dashboard
|
||||
assert "覆核建議:" in dashboard
|
||||
assert "review.catalog_review_guidance.action_hint" in dashboard
|
||||
assert 'data-review-action="needs_research"' in dashboard
|
||||
assert "人工閉環" in route_source
|
||||
assert "AI 挑品清單" in dashboard
|
||||
|
||||
@@ -60,6 +60,19 @@ def test_webcrumbs_host_data_api_allows_internal_key_when_login_disabled(monkeyp
|
||||
"matched_count": 88,
|
||||
"fresh_match_count": 70,
|
||||
"fresh_match_rate": 79.5,
|
||||
"decision_support_count": 105,
|
||||
"decision_support_rate": 14.7,
|
||||
"catalog_comparable_count": 12,
|
||||
"catalog_variant_review_count": 7,
|
||||
"catalog_unit_review_count": 3,
|
||||
"catalog_identity_review_count": 2,
|
||||
"catalog_review_plan": {
|
||||
"variant_review": 7,
|
||||
"unit_review": 3,
|
||||
"identity_review": 2,
|
||||
"total": 12,
|
||||
},
|
||||
"unit_comparable_count": 23,
|
||||
"stale_match_count": 18,
|
||||
"pending_match_count": 612,
|
||||
"writes_database": False,
|
||||
@@ -88,6 +101,14 @@ def test_webcrumbs_host_data_api_allows_internal_key_when_login_disabled(monkeyp
|
||||
assert payload["metadata"]["source"] == "competitor_intel_repository"
|
||||
assert payload["metadata"]["fresh_match_count"] == 70
|
||||
assert payload["metadata"]["fresh_match_rate"] == 79.5
|
||||
assert payload["metadata"]["decision_support_count"] == 105
|
||||
assert payload["metadata"]["decision_support_rate"] == 14.7
|
||||
assert payload["metadata"]["catalog_comparable_count"] == 12
|
||||
assert payload["metadata"]["catalog_variant_review_count"] == 7
|
||||
assert payload["metadata"]["catalog_unit_review_count"] == 3
|
||||
assert payload["metadata"]["catalog_identity_review_count"] == 2
|
||||
assert payload["metadata"]["catalog_review_plan"]["total"] == 12
|
||||
assert payload["metadata"]["unit_comparable_count"] == 23
|
||||
assert payload["metadata"]["stale_match_count"] == 18
|
||||
assert payload["metadata"]["pending_match_count"] == 612
|
||||
assert payload["boundary"]["auth_required"] is True
|
||||
|
||||
@@ -45,6 +45,21 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
||||
"fresh_match_rate": 79.5,
|
||||
"decision_ready_matches": 70,
|
||||
"decision_ready_rate": 9.8,
|
||||
"decision_support_count": 105,
|
||||
"decision_support_rate": 14.7,
|
||||
"decision_support_non_exact_count": 35,
|
||||
"catalog_comparable_count": 12,
|
||||
"catalog_comparable_rate": 1.7,
|
||||
"catalog_variant_review_count": 7,
|
||||
"catalog_unit_review_count": 3,
|
||||
"catalog_identity_review_count": 2,
|
||||
"catalog_review_plan": {
|
||||
"variant_review": 7,
|
||||
"unit_review": 3,
|
||||
"identity_review": 2,
|
||||
"total": 12,
|
||||
},
|
||||
"unit_comparable_count": 23,
|
||||
"stale_matches": 18,
|
||||
"pending": 612,
|
||||
},
|
||||
@@ -104,6 +119,18 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
|
||||
assert payload["metadata"]["identity_coverage_rate"] == 12.3
|
||||
assert payload["metadata"]["decision_ready_count"] == 70
|
||||
assert payload["metadata"]["decision_ready_rate"] == 9.8
|
||||
assert payload["metadata"]["decision_support_count"] == 105
|
||||
assert payload["metadata"]["decision_support_rate"] == 14.7
|
||||
assert payload["metadata"]["decision_support_non_exact_count"] == 35
|
||||
assert payload["metadata"]["catalog_comparable_count"] == 12
|
||||
assert payload["metadata"]["catalog_comparable_rate"] == 1.7
|
||||
assert payload["metadata"]["catalog_variant_review_count"] == 7
|
||||
assert payload["metadata"]["catalog_unit_review_count"] == 3
|
||||
assert payload["metadata"]["catalog_identity_review_count"] == 2
|
||||
assert payload["metadata"]["catalog_review_plan"]["variant_review"] == 7
|
||||
assert payload["metadata"]["catalog_review_plan"]["unit_review"] == 3
|
||||
assert payload["metadata"]["catalog_review_plan"]["identity_review"] == 2
|
||||
assert payload["metadata"]["unit_comparable_count"] == 23
|
||||
assert payload["metadata"]["fresh_match_count"] == 70
|
||||
assert payload["metadata"]["fresh_match_rate"] == 79.5
|
||||
assert payload["metadata"]["stale_match_count"] == 18
|
||||
@@ -124,7 +151,24 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
svc,
|
||||
"fetch_competitor_coverage",
|
||||
lambda engine: {"valid_matches": 3, "fresh_matches": 1, "stale_matches": 2, "pending": 9},
|
||||
lambda engine: {
|
||||
"valid_matches": 3,
|
||||
"fresh_matches": 1,
|
||||
"decision_support_count": 4,
|
||||
"catalog_comparable_count": 2,
|
||||
"catalog_variant_review_count": 1,
|
||||
"catalog_unit_review_count": 1,
|
||||
"catalog_identity_review_count": 0,
|
||||
"catalog_review_plan": {
|
||||
"variant_review": 1,
|
||||
"unit_review": 1,
|
||||
"identity_review": 0,
|
||||
"total": 2,
|
||||
},
|
||||
"unit_comparable_count": 1,
|
||||
"stale_matches": 2,
|
||||
"pending": 9,
|
||||
},
|
||||
)
|
||||
|
||||
payload = svc.build_webcrumbs_marketplace_host_data(engine=object(), limit=5)
|
||||
@@ -133,6 +177,13 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
|
||||
assert payload["aiCandidate"]["release_status"] == "blocked"
|
||||
assert payload["metadata"]["matched_count"] == 3
|
||||
assert payload["metadata"]["decision_ready_count"] == 1
|
||||
assert payload["metadata"]["decision_support_count"] == 4
|
||||
assert payload["metadata"]["catalog_comparable_count"] == 2
|
||||
assert payload["metadata"]["catalog_variant_review_count"] == 1
|
||||
assert payload["metadata"]["catalog_unit_review_count"] == 1
|
||||
assert payload["metadata"]["catalog_identity_review_count"] == 0
|
||||
assert payload["metadata"]["catalog_review_plan"]["total"] == 2
|
||||
assert payload["metadata"]["unit_comparable_count"] == 1
|
||||
assert payload["metadata"]["fresh_match_count"] == 1
|
||||
assert payload["metadata"]["stale_match_count"] == 2
|
||||
assert payload["metadata"]["pending_match_count"] == 9
|
||||
|
||||
Reference in New Issue
Block a user