新增市場情報樣本驗收契約
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s

This commit is contained in:
OoO
2026-05-19 00:18:10 +08:00
parent dddafc579b
commit cd08dd7f97
9 changed files with 435 additions and 32 deletions

View File

@@ -19,6 +19,7 @@ def build_deployment_readiness_preview(
mcp_fetch_gate = service.build_mcp_fetch_gate()
scheduler_plan = service.build_scheduler_plan()
manual_sample_plan = service.build_manual_sample_plan()
manual_sample_acceptance = service.build_manual_sample_acceptance()
match_review_plan = service.build_match_review_plan()
opportunity_plan = service.build_opportunity_plan()
opportunity_scoring_plan = service.build_opportunity_scoring_plan()
@@ -86,6 +87,15 @@ def build_deployment_readiness_preview(
and not manual_sample_plan["database_commit_executed"]
and not manual_sample_plan["scheduler_attached"]
),
"manual_sample_acceptance_preview_safe": bool(
manual_sample_acceptance["mode"] == "manual_sample_acceptance_preview"
and not manual_sample_acceptance["sample_result_loaded"]
and not manual_sample_acceptance["candidate_import_allowed"]
and not manual_sample_acceptance["external_network_executed"]
and not manual_sample_acceptance["database_write_executed"]
and not manual_sample_acceptance["database_commit_executed"]
and not manual_sample_acceptance["scheduler_attached"]
),
"match_review_plan_preview_safe": bool(
match_review_plan["mode"] == "match_review_plan_preview"
and not match_review_plan["review_queue_created"]
@@ -289,6 +299,7 @@ def build_deployment_readiness_preview(
"/api/market_intel/mcp_fetch_gate",
"/api/market_intel/scheduler_plan",
"/api/market_intel/manual_sample_plan",
"/api/market_intel/manual_sample_acceptance",
"/api/market_intel/match_review_plan",
"/api/market_intel/opportunity_plan",
"/api/market_intel/opportunity_scoring_plan",
@@ -323,6 +334,7 @@ def build_deployment_readiness_preview(
"mcp_fetch_gate": mcp_fetch_gate,
"scheduler_plan": scheduler_plan,
"manual_sample_plan": manual_sample_plan,
"manual_sample_acceptance": manual_sample_acceptance,
"match_review_plan": match_review_plan,
"opportunity_plan": opportunity_plan,
"opportunity_scoring_plan": opportunity_scoring_plan,

View File

@@ -0,0 +1,160 @@
"""市場情報人工 sample fetch 結果驗收契約。
本模組只定義第一次 sample fetch 回來後的驗收欄位、門檻與人工決策;
不讀外部網站、不查 DB、不寫 DB、不掛排程。
"""
REQUIRED_RESULT_FIELDS = (
"batch_id",
"platform_code",
"source_key",
"source_url",
"status",
"status_code",
"content_length",
"page_hash",
"title",
"diagnostics",
)
REQUIRED_DIAGNOSTIC_FIELDS = (
"link_count",
"same_host_link_count",
"campaign_link_candidates",
)
def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan):
"""建立人工樣本結果驗收契約;不載入 sample result。"""
gate_checks = {
"manual_sample_plan_present": bool(manual_sample_plan),
"sample_fetch_not_executed_by_api": not bool(
manual_sample_plan.get("sample_fetch_executed")
),
"external_network_blocked_in_preview": not bool(
manual_sample_plan.get("external_network_executed")
),
"database_write_still_blocked": not bool(
getattr(runtime_status, "database_write_allowed", False)
),
"scheduler_detached": not bool(
getattr(runtime_status, "scheduler_attached", False)
),
}
blocked_reasons = [
key for key, passed in gate_checks.items()
if not passed
]
blocked_reasons.extend(
[
"sample_result_not_loaded",
"manual_review_required_before_import",
]
)
return {
"mode": "manual_sample_acceptance_preview",
"contract_ready": True,
"sample_result_loaded": False,
"sample_result_accepted": False,
"candidate_import_allowed": False,
"external_network_executed": False,
"database_connection_opened": False,
"database_session_created": False,
"database_write_executed": False,
"database_commit_executed": False,
"scheduler_attached": False,
"writes_executed": False,
"would_write_database": False,
"gate_checks": gate_checks,
"blocked_reasons": blocked_reasons,
"required_result_fields": list(REQUIRED_RESULT_FIELDS),
"required_diagnostic_fields": list(REQUIRED_DIAGNOSTIC_FIELDS),
"acceptance_thresholds": {
"http_status_min": 200,
"http_status_max": 299,
"minimum_content_length": 500,
"page_hash_length": 64,
"minimum_title_length": 2,
"minimum_link_count": 1,
"minimum_campaign_candidates": 1,
"accepted_candidate_bands": ["high", "medium"],
},
"acceptance_checks": [
{
"key": "http_status_ok",
"label": "HTTP status 必須為 2xxredirect / 403 / 429 / 5xx 都不得進入候選導入",
"status": "not_evaluated",
},
{
"key": "content_has_body",
"label": "content_length 必須超過最低門檻,避免只拿到 bot challenge 或空頁",
"status": "not_evaluated",
},
{
"key": "page_fingerprint_present",
"label": "page_hash 必須存在,後續才能追蹤同頁變化",
"status": "not_evaluated",
},
{
"key": "parser_diagnostics_present",
"label": "diagnostics 必須包含 title、link_count 與 campaign_link_candidates",
"status": "not_evaluated",
},
{
"key": "candidate_quality_reviewed",
"label": "至少 1 筆 high/medium 候選需經人工確認,才可進入候選活動審核",
"status": "not_evaluated",
},
],
"reject_conditions": [
{
"key": "anti_bot_or_login_wall",
"label": "頁面出現登入牆、驗證碼、bot challenge、會員頁或購物車內容",
},
{
"key": "unexpected_personal_data",
"label": "頁面含非公開個資、帳號資料或訂單資訊",
},
{
"key": "too_many_failures",
"label": "同平台樣本連續失敗時,不做快速重試,先回頭審查 adapter 入口",
},
{
"key": "no_campaign_candidates",
"label": "沒有任何 high/medium campaign candidate 時,不建立活動候選",
},
],
"operator_decisions": [
{
"key": "approve_candidate_preview",
"label": "樣本可進入候選活動人工審核,但仍不得寫 market_campaigns",
"write_status": "blocked",
},
{
"key": "revise_adapter_source",
"label": "入口不適合時,調整 adapter source 後重新跑單一樣本",
"write_status": "blocked",
},
{
"key": "reject_platform_for_now",
"label": "平台風險或阻擋過高時,暫停該平台,只保留其他平台預覽",
"write_status": "blocked",
},
],
"promotion_sequence": [
"先把 sample result 與 diagnostics 以人工方式審核",
"通過後只開 candidate preview不建立正式 campaign/product",
"累積至少 2 個平台樣本通過後,再設計候選活動審核資料流",
"任何寫入 market_* 前仍需獨立 approval 與 rollback plan",
],
"safe_boundaries": [
"do_not_accept_login_or_member_pages",
"do_not_accept_anti_bot_challenge_pages",
"do_not_import_candidates_without_human_review",
"do_not_write_market_tables_from_acceptance_preview",
"do_not_attach_scheduler_from_sample_result",
"do_not_touch_momo_db_lifecycle",
],
}

View File

@@ -33,6 +33,9 @@ from services.market_intel.mcp_readiness import build_mcp_readiness_plan
from services.market_intel.manual_sample_plan import (
build_manual_sample_fetch_plan_preview,
)
from services.market_intel.manual_sample_acceptance import (
build_manual_sample_acceptance_preview,
)
from services.market_intel.migration_blueprint import build_migration_blueprint
from services.market_intel.migration_catalog_review import (
build_migration_catalog_review_preview,
@@ -95,7 +98,7 @@ class MarketIntelRuntimeStatus:
class MarketIntelService:
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
phase = "phase_47_manual_sample_fetch_plan"
phase = "phase_48_manual_sample_acceptance_contract"
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
return MarketIntelRuntimeStatus(
@@ -435,6 +438,15 @@ class MarketIntelService:
plan["phase"] = self.phase
return plan
def build_manual_sample_acceptance(self):
"""回報人工 sample result 驗收契約;不載入外部結果、不寫 DB。"""
acceptance = build_manual_sample_acceptance_preview(
runtime_status=self.get_runtime_status(),
manual_sample_plan=self.build_manual_sample_plan(),
)
acceptance["phase"] = self.phase
return acceptance
def build_match_review_plan(self):
"""回報商品比對審核計畫;不建立 queue、不自動確認。"""
schema_smoke = self.build_schema_smoke()