Merge remote-tracking branch 'origin/main' into codex/growth-p0-sales-source-reconciliation-dev

This commit is contained in:
ogt
2026-07-15 17:25:42 +08:00
3 changed files with 101 additions and 7 deletions

View File

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

View File

@@ -287,14 +287,13 @@ def activate_external_market_source_after_canary(
and bool(item["same_item_reconciliation"].get("independent_verifier_passed")) and bool(item["same_item_reconciliation"].get("independent_verifier_passed"))
for item in candidates for item in candidates
) )
can_activate = bool( canary_evidence_verified = bool(
independent_readback.get("success") independent_readback.get("success")
and len(candidates) >= threshold
and readback_count == len(candidates) and readback_count == len(candidates)
and guards_passed and guards_passed
and independent_verifiers_passed and independent_verifiers_passed
) )
if not can_activate: if not canary_evidence_verified:
return { return {
"success": True, "success": True,
"status": "canary_threshold_not_met", "status": "canary_threshold_not_met",
@@ -305,6 +304,72 @@ def activate_external_market_source_after_canary(
"guards_passed": guards_passed, "guards_passed": guards_passed,
"independent_verifiers_passed": independent_verifiers_passed, "independent_verifiers_passed": independent_verifiers_passed,
"applied": False, "applied": False,
"state_changed": False,
"writes_database_count": 0,
}
with engine.begin() as conn:
_ensure_external_market_source_seeds(conn)
current = (
conn.execute(
text(
"""
SELECT status, enabled, write_enabled
FROM external_market_sources
WHERE code = :source_code
"""
),
{"source_code": source_code},
)
.mappings()
.first()
)
if not current:
return {
"success": False,
"status": "source_registry_missing",
"source_code": source_code,
"applied": False,
"state_changed": False,
"writes_database_count": 0,
}
already_active = bool(
str(current.get("status") or "") == "active"
and bool(current.get("enabled"))
and bool(current.get("write_enabled"))
)
if already_active:
return {
"success": True,
"status": "already_active_verified",
"source_code": source_code,
"candidate_count": len(candidates),
"readback_pass_count": readback_count,
"minimum_verified_count": threshold,
"guards_passed": guards_passed,
"independent_verifiers_passed": independent_verifiers_passed,
"applied": True,
"state_changed": False,
"previous_state": dict(current),
"observed_state": dict(current),
"identity": dict(identity or {}),
"writes_database_count": 0,
}
if len(candidates) < threshold:
return {
"success": True,
"status": "canary_threshold_not_met",
"source_code": source_code,
"candidate_count": len(candidates),
"readback_pass_count": readback_count,
"minimum_verified_count": threshold,
"guards_passed": guards_passed,
"independent_verifiers_passed": independent_verifiers_passed,
"applied": False,
"state_changed": False,
"observed_state": dict(current),
"writes_database_count": 0, "writes_database_count": 0,
} }
@@ -330,6 +395,7 @@ def activate_external_market_source_after_canary(
"status": "source_registry_missing", "status": "source_registry_missing",
"source_code": source_code, "source_code": source_code,
"applied": False, "applied": False,
"state_changed": False,
"writes_database_count": 0, "writes_database_count": 0,
} }
conn.execute( conn.execute(
@@ -382,6 +448,7 @@ def activate_external_market_source_after_canary(
"observed_state": dict(observed or {}), "observed_state": dict(observed or {}),
"identity": dict(identity or {}), "identity": dict(identity or {}),
"writes_database_count": 1 if applied else 0, "writes_database_count": 1 if applied else 0,
"state_changed": applied,
} }
@@ -452,7 +519,13 @@ def rollback_external_market_source_canary(
{"offer_id": offer_id}, {"offer_id": offer_id},
) )
if activation_receipt.get("applied") and previous: state_changed = bool(
activation_receipt.get(
"state_changed",
activation_receipt.get("applied"),
)
)
if state_changed and previous:
conn.execute( conn.execute(
text( text(
""" """
@@ -478,10 +551,10 @@ def rollback_external_market_source_canary(
"status": "rolled_back_and_quarantined", "status": "rolled_back_and_quarantined",
"source_code": source_code, "source_code": source_code,
"quarantined_offer_count": len(quarantined_ids), "quarantined_offer_count": len(quarantined_ids),
"source_state_restored": bool(activation_receipt.get("applied") and previous), "source_state_restored": bool(state_changed and previous),
"identity": dict(identity or {}), "identity": dict(identity or {}),
"writes_database_count": len(quarantined_ids) "writes_database_count": len(quarantined_ids)
+ (1 if activation_receipt.get("applied") and previous else 0), + (1 if state_changed and previous else 0),
} }

View File

@@ -797,6 +797,27 @@ def test_targeted_marketplace_canary_activates_and_rollback_quarantines(monkeypa
assert yahoo["has_runtime_data"] is True assert yahoo["has_runtime_data"] is True
assert yahoo["write_enabled"] is True assert yahoo["write_enabled"] is True
one_candidate_readback = readback_external_offer_candidates(
engine,
candidates[:1],
source_code="yahoo_shopping",
ingestion_method="public_product_jsonld",
)
idempotent_activation = (
canary_service.activate_external_market_source_after_canary(
engine,
source_code="yahoo_shopping",
candidates=candidates[:1],
independent_readback=one_candidate_readback,
identity=identity,
minimum_verified_count=2,
)
)
assert idempotent_activation["status"] == "already_active_verified"
assert idempotent_activation["applied"] is True
assert idempotent_activation["state_changed"] is False
assert idempotent_activation["writes_database_count"] == 0
rollback = canary_service.rollback_external_market_source_canary( rollback = canary_service.rollback_external_market_source_canary(
engine, engine,
source_code="yahoo_shopping", source_code="yahoo_shopping",