31ce04c655
Merge the beta-ready Yao Meta Skill architecture, report, evidence gate, and release-boundary updates.\n\nRelease boundary: beta/public testing is allowed; formal world-class, fully reviewed, or superiority claims remain blocked until the pending evidence gates are accepted.
544 lines
24 KiB
Python
544 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Gate evaluation contract for Review Studio 2.0."""
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from review_studio_gate_contract import (
|
|
GATE_WEIGHTS,
|
|
REVIEW_STUDIO_GATE_KEYS,
|
|
add_blockers_from_gate,
|
|
gate_contract,
|
|
min_output_cases,
|
|
status_label,
|
|
weighted_score,
|
|
)
|
|
from review_studio_gate_helpers import (
|
|
build_output_lab_gate,
|
|
fallback_permission_governance,
|
|
gate,
|
|
report_link,
|
|
target_maturity,
|
|
)
|
|
|
|
|
|
SCRIPT_INTERFACE = "internal-module"
|
|
SCRIPT_INTERFACE_REASON = "Imported by render_review_studio.py to keep Review Studio gate evaluation separate from HTML rendering."
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def build_gates(skill_dir: Path, output_html: Path, data: dict[str, dict[str, Any]]) -> list[dict[str, str]]:
|
|
overview = data["overview"]
|
|
maturity = target_maturity(skill_dir, overview)
|
|
gates: list[dict[str, str]] = []
|
|
|
|
intent = data["intent_confidence"]
|
|
intent_score = int(intent.get("score", 0) or 0)
|
|
intent_status = "pass" if intent.get("gate_passed") or intent_score >= 75 else "warn"
|
|
gates.append(
|
|
gate(
|
|
"intent-canvas",
|
|
"意图画布",
|
|
intent_status,
|
|
f"intent confidence {intent_score}/100; {intent.get('recommended_action', 'review current intent frame')}",
|
|
"reports/intent-confidence.json",
|
|
report_link(output_html, skill_dir, "reports/intent-confidence.md"),
|
|
)
|
|
)
|
|
|
|
route = data["route_scorecard"]
|
|
route_summary = route.get("summary", {})
|
|
misroutes = int(route_summary.get("misroute_count", len(route.get("misroutes", []))) or 0)
|
|
ambiguous = int(route_summary.get("ambiguous_case_count", len(route.get("ambiguous_cases", []))) or 0)
|
|
if not route:
|
|
route_status = "warn"
|
|
route_detail = "route scorecard is missing; run route-scorecard before release review"
|
|
else:
|
|
route_status = "block" if misroutes else ("warn" if ambiguous else "pass")
|
|
route_detail = f"{route_summary.get('total_cases', 0)} trigger cases; {misroutes} misroutes; {ambiguous} ambiguous"
|
|
gates.append(
|
|
gate(
|
|
"trigger-lab",
|
|
"触发实验",
|
|
route_status,
|
|
route_detail,
|
|
"reports/route_scorecard.json",
|
|
report_link(output_html, skill_dir, "reports/route_scorecard.md"),
|
|
)
|
|
)
|
|
|
|
gates.append(
|
|
build_output_lab_gate(
|
|
skill_dir,
|
|
output_html,
|
|
maturity,
|
|
data["output_quality"],
|
|
data["output_execution"],
|
|
data["output_blind_review"],
|
|
data["output_review_adjudication"],
|
|
)
|
|
)
|
|
|
|
context = data["context_budget"]
|
|
context_stats = context.get("stats", {})
|
|
context_status = "pass" if context.get("ok") else "block"
|
|
if context.get("warnings"):
|
|
context_status = "warn" if context_status == "pass" else context_status
|
|
if not context:
|
|
context_status = "warn"
|
|
large_deferred_dirs = context_stats.get("large_deferred_resource_dirs", []) or []
|
|
top_deferred = "none"
|
|
if large_deferred_dirs:
|
|
first = large_deferred_dirs[0]
|
|
top_deferred = f"{first.get('path', 'resource')} {first.get('estimated_tokens', 'n/a')}"
|
|
deferred_governance = context_stats.get("deferred_resource_governance", {}) if isinstance(context_stats, dict) else {}
|
|
governance_status = deferred_governance.get("status", "unknown") if isinstance(deferred_governance, dict) else "unknown"
|
|
context_detail = (
|
|
f"initial load {context_stats.get('estimated_initial_load_tokens', 'n/a')}/"
|
|
f"{context_stats.get('context_budget_limit', 'n/a')}; "
|
|
f"deferred {context_stats.get('deferred_resource_tokens', 'n/a')}/"
|
|
f"{context_stats.get('deferred_resource_warn_threshold', 'n/a')}; "
|
|
f"top deferred {top_deferred}; "
|
|
f"resource governance {governance_status}; "
|
|
f"quality density {context_stats.get('quality_density', 'n/a')}"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"context-budget",
|
|
"上下文",
|
|
context_status,
|
|
context_detail,
|
|
"reports/context_budget.json",
|
|
report_link(output_html, skill_dir, "reports/context_budget.md"),
|
|
)
|
|
)
|
|
|
|
conformance = data["conformance"]
|
|
conformance_summary = conformance.get("summary", {})
|
|
fail_count = int(conformance_summary.get("fail_count", 0) or 0)
|
|
if not conformance:
|
|
conformance_status = "warn"
|
|
conformance_detail = "runtime conformance matrix is missing"
|
|
else:
|
|
conformance_status = "block" if fail_count else "pass"
|
|
conformance_detail = f"{conformance_summary.get('pass_count', 0)} / {conformance_summary.get('target_count', 0)} targets pass"
|
|
gates.append(
|
|
gate(
|
|
"runtime-matrix",
|
|
"运行矩阵",
|
|
conformance_status,
|
|
conformance_detail,
|
|
"reports/conformance_matrix.json",
|
|
report_link(output_html, skill_dir, "reports/conformance_matrix.md"),
|
|
)
|
|
)
|
|
|
|
trust = data["trust"]
|
|
trust_summary = trust.get("summary", {})
|
|
if not trust:
|
|
trust_status = "warn"
|
|
trust_detail = "security trust report is missing"
|
|
else:
|
|
trust_status = "block" if trust.get("failures") else ("warn" if trust.get("warnings") else "pass")
|
|
trust_detail = (
|
|
f"{trust_summary.get('secret_findings', 0)} secrets; "
|
|
f"{trust_summary.get('script_count', 0)} scripts; "
|
|
f"{trust_summary.get('network_script_count', 0)} network-capable scripts; "
|
|
f"{trust_summary.get('help_smoke_failed_count', 0)} help smoke failures"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"trust-report",
|
|
"信任报告",
|
|
trust_status,
|
|
trust_detail,
|
|
"reports/security_trust_report.json",
|
|
report_link(output_html, skill_dir, "reports/security_trust_report.md"),
|
|
)
|
|
)
|
|
|
|
python_compat = data["python_compatibility"]
|
|
python_compat_summary = python_compat.get("summary", {})
|
|
if not python_compat:
|
|
python_compat_status = "warn"
|
|
python_compat_detail = "Python compatibility report is missing"
|
|
else:
|
|
issue_count = int(python_compat_summary.get("issue_count", 0) or 0)
|
|
syntax_error_count = int(python_compat_summary.get("syntax_error_count", 0) or 0)
|
|
fstring_violation_count = int(python_compat_summary.get("fstring_311_violation_count", 0) or 0)
|
|
python_compat_status = (
|
|
"block"
|
|
if not python_compat.get("ok", True) or issue_count or syntax_error_count or fstring_violation_count
|
|
else "pass"
|
|
)
|
|
python_compat_detail = (
|
|
f"Python {python_compat_summary.get('target_python', '3.11')}; "
|
|
f"{python_compat_summary.get('file_count', 0)} files; "
|
|
f"{issue_count} compatibility issues; "
|
|
f"{syntax_error_count} syntax; "
|
|
f"{fstring_violation_count} f-string 3.11 hazards"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"python-compat",
|
|
"Python 兼容",
|
|
python_compat_status,
|
|
python_compat_detail,
|
|
"reports/python_compatibility.json",
|
|
report_link(output_html, skill_dir, "reports/python_compatibility.md"),
|
|
)
|
|
)
|
|
|
|
architecture = data["architecture_maintainability"]
|
|
architecture_summary = architecture.get("summary", {})
|
|
if not architecture:
|
|
if maturity in {"library", "governed"}:
|
|
architecture_status = "warn"
|
|
architecture_detail = "architecture maintainability report is missing for a reusable package"
|
|
else:
|
|
architecture_status = "pass"
|
|
architecture_detail = "architecture maintainability report is optional until code surface grows"
|
|
else:
|
|
hotspot_count = int(architecture_summary.get("hotspot_count", 0) or 0)
|
|
watchlist_count = int(architecture_summary.get("watchlist_count", 0) or 0)
|
|
early_watchlist_count = int(architecture_summary.get("early_watchlist_count", 0) or 0)
|
|
blocker_count = int(architecture_summary.get("blocker_count", 0) or 0)
|
|
architecture_status = "block" if not architecture.get("ok", True) or blocker_count else ("warn" if hotspot_count else "pass")
|
|
hotspot_label = "hotspot" if hotspot_count == 1 else "hotspots"
|
|
watchlist_label = "watchlist file" if watchlist_count == 1 else "watchlist files"
|
|
early_label = "early watch file" if early_watchlist_count == 1 else "early watch files"
|
|
blocker_label = "blocker" if blocker_count == 1 else "blockers"
|
|
architecture_detail = (
|
|
f"{architecture_summary.get('python_file_count', 0)} Python files; "
|
|
f"{hotspot_count} {hotspot_label}; "
|
|
f"{watchlist_count} {watchlist_label}; "
|
|
f"{early_watchlist_count} {early_label}; "
|
|
f"{blocker_count} {blocker_label}; "
|
|
f"largest {architecture_summary.get('largest_file_lines', 0)} lines; "
|
|
f"{architecture_summary.get('command_handler_count', 0)} CLI handlers; "
|
|
f"{architecture_summary.get('entrypoint_command_handler_count', 0)} in entrypoint"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"architecture-maintainability",
|
|
"架构维护",
|
|
architecture_status,
|
|
architecture_detail,
|
|
"reports/architecture_maintainability.json",
|
|
report_link(output_html, skill_dir, "reports/architecture_maintainability.md"),
|
|
)
|
|
)
|
|
|
|
permission_governance = trust.get("permission_governance", {}) if isinstance(trust.get("permission_governance", {}), dict) else {}
|
|
if trust and not permission_governance:
|
|
permission_governance = fallback_permission_governance(skill_dir)
|
|
if not trust:
|
|
permission_status = "warn"
|
|
permission_detail = "permission governance evidence is missing because trust report is missing"
|
|
elif not permission_governance:
|
|
permission_status = "warn"
|
|
permission_detail = "permission governance evidence is missing from trust report"
|
|
else:
|
|
required = int(permission_governance.get("required_count", 0) or 0)
|
|
approved = int(permission_governance.get("approval_count", 0) or 0)
|
|
gaps = (
|
|
int(permission_governance.get("missing_count", 0) or 0)
|
|
+ int(permission_governance.get("invalid_count", 0) or 0)
|
|
+ int(permission_governance.get("expired_count", 0) or 0)
|
|
)
|
|
if gaps:
|
|
permission_status = "block" if maturity == "governed" else "warn"
|
|
else:
|
|
permission_status = "pass"
|
|
required_names = ", ".join(permission_governance.get("required_capabilities", []) or []) or "none"
|
|
permission_detail = f"{approved}/{required} permissions approved; gaps {gaps}; required {required_names}"
|
|
gates.append(
|
|
gate(
|
|
"permission-gates",
|
|
"权限批准",
|
|
permission_status,
|
|
permission_detail,
|
|
"reports/security_trust_report.json + security/permission_policy.json",
|
|
report_link(output_html, skill_dir, "security/permission_policy.md"),
|
|
)
|
|
)
|
|
|
|
runtime_permissions = data["runtime_permissions"]
|
|
runtime_permissions_summary = runtime_permissions.get("summary", {})
|
|
if not runtime_permissions:
|
|
runtime_permission_status = "block" if maturity == "governed" else "warn"
|
|
runtime_permission_detail = "runtime permission probe report is missing"
|
|
elif runtime_permissions.get("failures"):
|
|
runtime_permission_status = "block"
|
|
runtime_permission_detail = f"{runtime_permissions_summary.get('failure_count', len(runtime_permissions.get('failures', [])))} runtime permission probe failures"
|
|
else:
|
|
runtime_permission_status = "pass"
|
|
runtime_permission_detail = (
|
|
f"{runtime_permissions_summary.get('pass_count', 0)}/{runtime_permissions_summary.get('target_count', 0)} targets probed; "
|
|
f"native {runtime_permissions_summary.get('native_enforcement_count', 0)}; "
|
|
f"metadata fallback {runtime_permissions_summary.get('metadata_fallback_count', 0)}; "
|
|
f"installer {runtime_permissions_summary.get('installer_enforcement_pass_count', 0)}; "
|
|
f"residual risks {runtime_permissions_summary.get('residual_risk_count', 0)}"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"permission-runtime",
|
|
"权限探针",
|
|
runtime_permission_status,
|
|
runtime_permission_detail,
|
|
"reports/runtime_permission_probes.json",
|
|
report_link(output_html, skill_dir, "reports/runtime_permission_probes.md"),
|
|
)
|
|
)
|
|
|
|
atlas = data["atlas"]
|
|
atlas_summary = atlas.get("summary", {})
|
|
actionable_route_collisions = int(
|
|
atlas_summary.get("actionable_route_collision_count", atlas_summary.get("route_collision_count", 0)) or 0
|
|
)
|
|
actionable_owner_gaps = int(atlas_summary.get("actionable_owner_gap_count", atlas_summary.get("owner_gap_count", 0)) or 0)
|
|
actionable_stale = int(atlas_summary.get("actionable_stale_count", atlas_summary.get("stale_count", 0)) or 0)
|
|
actionable_drift = int(atlas_summary.get("actionable_drift_signal_count", atlas_summary.get("drift_signal_count", 0)) or 0)
|
|
atlas_issues = actionable_route_collisions + actionable_owner_gaps + actionable_stale + actionable_drift
|
|
if not atlas:
|
|
atlas_status = "warn"
|
|
atlas_detail = "skill atlas is missing; portfolio-level conflicts are unknown"
|
|
else:
|
|
atlas_status = "warn" if atlas_issues else "pass"
|
|
atlas_detail = (
|
|
f"{atlas_summary.get('skill_count', 0)} skills, "
|
|
f"{atlas_summary.get('actionable_skill_count', atlas_summary.get('skill_count', 0))} actionable; "
|
|
f"{actionable_route_collisions} actionable route collisions; "
|
|
f"{actionable_owner_gaps} actionable owner gaps; "
|
|
f"{actionable_stale} actionable stale; "
|
|
f"{actionable_drift} actionable drift; "
|
|
f"{atlas_summary.get('non_actionable_issue_count', 0)} scoped non-actionable issues"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"skill-atlas",
|
|
"组合治理",
|
|
atlas_status,
|
|
atlas_detail,
|
|
"reports/skill_atlas.json",
|
|
report_link(output_html, skill_dir, "reports/skill_atlas.html"),
|
|
)
|
|
)
|
|
|
|
adoption = data["adoption_drift"]
|
|
adoption_summary = adoption.get("summary", {})
|
|
daily_skillops = data.get("daily_skillops", {})
|
|
weekly_curator = data.get("weekly_curator", {})
|
|
daily_summary = daily_skillops.get("summary", {}) if isinstance(daily_skillops, dict) else {}
|
|
weekly_summary = weekly_curator.get("summary", {}) if isinstance(weekly_curator, dict) else {}
|
|
if not adoption:
|
|
adoption_status = "warn"
|
|
adoption_detail = "adoption drift report is missing; real usage impact is unknown"
|
|
elif adoption.get("failures"):
|
|
adoption_status = "block"
|
|
adoption_detail = f"telemetry privacy or schema failures: {len(adoption.get('failures', []))}"
|
|
else:
|
|
risk_band = adoption_summary.get("risk_band", "no-data")
|
|
adoption_status = "warn" if risk_band in {"no-data", "medium", "high"} else "pass"
|
|
adoption_detail = (
|
|
f"{adoption_summary.get('event_count', 0)} metadata events; "
|
|
f"adoption {adoption_summary.get('adoption_rate', 0)}; "
|
|
f"missed {adoption_summary.get('missed_trigger_count', 0)}; "
|
|
f"bad-output {adoption_summary.get('bad_output_count', 0)}; "
|
|
f"risk {risk_band}"
|
|
)
|
|
skillops_parts = []
|
|
skillops_blocked = False
|
|
for label, report, summary in [
|
|
("daily", daily_skillops, daily_summary),
|
|
("weekly", weekly_curator, weekly_summary),
|
|
]:
|
|
if not report:
|
|
continue
|
|
writes_source = bool(summary.get("writes_source_files") or report.get("writes_source_files"))
|
|
auto_patch = bool(summary.get("auto_patch_enabled") or report.get("auto_patch_enabled"))
|
|
failure_count = int(summary.get("failure_count", 0) or 0)
|
|
if writes_source or auto_patch or failure_count:
|
|
skillops_blocked = True
|
|
if label == "daily":
|
|
skillops_parts.append(
|
|
f"daily proposals {summary.get('proposal_count', 0)}; "
|
|
f"daily decision {summary.get('decision', report.get('decision', 'unknown'))}; "
|
|
f"daily release lock {str(summary.get('release_lock_ready', False)).lower()}"
|
|
)
|
|
else:
|
|
skillops_parts.append(
|
|
f"weekly queue {summary.get('unique_opportunity_count', 0)} unique; "
|
|
f"weekly ready {summary.get('ready_for_approval_review_count', 0)}; "
|
|
f"weekly top {summary.get('top_score', 0)}; "
|
|
f"weekly release lock {str(summary.get('release_lock_ready', False)).lower()}"
|
|
)
|
|
if skillops_blocked:
|
|
adoption_status = "block"
|
|
if skillops_parts:
|
|
adoption_detail = adoption_detail + "; " + "; ".join(skillops_parts)
|
|
gates.append(
|
|
gate(
|
|
"operations-loop",
|
|
"运营回路",
|
|
adoption_status,
|
|
adoption_detail,
|
|
"reports/adoption_drift_report.json + reports/skillops/daily + reports/skillops/weekly",
|
|
report_link(output_html, skill_dir, "reports/adoption_drift_report.md"),
|
|
)
|
|
)
|
|
|
|
waiver = data["review_waivers"]
|
|
waiver_summary = waiver.get("summary", {})
|
|
active_covered = set(waiver_summary.get("covered_gate_keys", []) or [])
|
|
prior_blockers = [item for item in gates if item["status"] == "block"]
|
|
prior_warnings = [item for item in gates if item["status"] == "warn"]
|
|
unwaived_warnings = [item for item in prior_warnings if item["key"] not in active_covered]
|
|
if not waiver:
|
|
waiver_status = "warn"
|
|
waiver_detail = "review waiver ledger is missing; warning acceptance is not auditable"
|
|
elif waiver.get("failures"):
|
|
waiver_status = "block"
|
|
waiver_detail = f"{len(waiver.get('failures', []))} invalid waiver records"
|
|
elif prior_blockers:
|
|
waiver_status = "block"
|
|
waiver_detail = f"{len(prior_blockers)} blocker gates cannot be waived in v0"
|
|
elif unwaived_warnings:
|
|
waiver_status = "warn"
|
|
waiver_detail = (
|
|
f"{waiver_summary.get('active_count', 0)} active waivers; "
|
|
f"{len(unwaived_warnings)} warning gates still need reviewer decision"
|
|
)
|
|
else:
|
|
waiver_status = "pass"
|
|
waiver_detail = f"{waiver_summary.get('active_count', 0)} active waivers cover current warnings"
|
|
gates.append(
|
|
gate(
|
|
"review-waivers",
|
|
"人工批准",
|
|
waiver_status,
|
|
waiver_detail,
|
|
"reports/review_waivers.json",
|
|
report_link(output_html, skill_dir, "reports/review_waivers.md"),
|
|
)
|
|
)
|
|
|
|
world_class_ledger = data.get("world_class_evidence_ledger", {})
|
|
world_class_summary = world_class_ledger.get("summary", {}) if isinstance(world_class_ledger, dict) else {}
|
|
source_check_count = int(world_class_summary.get("source_check_count", 0) or 0)
|
|
source_pass_count = int(world_class_summary.get("source_pass_count", 0) or 0)
|
|
source_blocked_count = int(world_class_summary.get("source_blocked_count", 0) or 0)
|
|
source_check_detail = (
|
|
f"source checks {source_pass_count}/{source_check_count} pass; {source_blocked_count} blocked"
|
|
if source_check_count
|
|
else "source checks unavailable"
|
|
)
|
|
if not world_class_ledger and maturity == "governed":
|
|
world_class_status = "warn"
|
|
world_class_detail = "world-class evidence ledger is missing; public world-class readiness cannot be claimed"
|
|
elif not world_class_ledger:
|
|
world_class_status = "pass"
|
|
world_class_detail = "world-class evidence ledger is optional until governed or public world-class readiness is claimed"
|
|
elif world_class_summary.get("ready_to_claim_world_class") is True:
|
|
world_class_status = "pass"
|
|
world_class_detail = (
|
|
f"{world_class_summary.get('accepted_count', 0)} accepted evidence entries; "
|
|
f"{world_class_summary.get('pending_count', 0)} pending; "
|
|
f"{source_check_detail}"
|
|
)
|
|
else:
|
|
world_class_status = "warn"
|
|
world_class_detail = (
|
|
f"{world_class_summary.get('pending_count', 0)} pending world-class evidence entries; "
|
|
f"{world_class_summary.get('human_pending_count', 0)} human pending; "
|
|
f"{world_class_summary.get('external_pending_count', 0)} external pending; "
|
|
f"{source_check_detail}; "
|
|
f"overclaim guard {str(world_class_summary.get('overclaim_guard_active', False)).lower()}"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"world-class-evidence",
|
|
"世界证据",
|
|
world_class_status,
|
|
world_class_detail,
|
|
"reports/world_class_evidence_ledger.json",
|
|
report_link(output_html, skill_dir, "reports/world_class_evidence_ledger.md"),
|
|
)
|
|
)
|
|
|
|
registry = data["registry"]
|
|
install = data["install_simulation"]
|
|
if not registry:
|
|
if maturity in {"library", "governed"}:
|
|
registry_status = "warn"
|
|
registry_detail = "registry audit is missing; package metadata is not reviewable"
|
|
else:
|
|
registry_status = "pass"
|
|
registry_detail = "registry audit is optional until team distribution is required"
|
|
else:
|
|
compatibility = registry.get("package", {}).get("compatibility", {})
|
|
pass_count = sum(1 for status in compatibility.values() if status == "pass")
|
|
registry_status = "block" if registry.get("failures") else ("warn" if registry.get("warnings") else "pass")
|
|
registry_detail = (
|
|
f"{registry.get('package', {}).get('name', 'package')} "
|
|
f"{registry.get('package', {}).get('version', 'n/a')}; "
|
|
f"{pass_count}/{len(compatibility)} compatibility entries pass"
|
|
)
|
|
if install:
|
|
install_summary = install.get("summary", {})
|
|
permission_failures = int(install_summary.get("installer_permission_failure_count", 0) or 0)
|
|
if install.get("failures") or permission_failures:
|
|
registry_status = "block"
|
|
registry_detail += (
|
|
f"; install {'pass' if install.get('ok') else 'fail'}"
|
|
f" with {install_summary.get('adapter_count', 0)} adapters"
|
|
f"; installer permissions {install_summary.get('installer_permission_enforced_count', 0)} enforced"
|
|
f" / {permission_failures} failures"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"registry-audit",
|
|
"注册审计",
|
|
registry_status,
|
|
registry_detail,
|
|
"reports/registry_audit.json + reports/install_simulation.json",
|
|
report_link(output_html, skill_dir, "reports/registry_audit.md"),
|
|
)
|
|
)
|
|
|
|
promotion = data["promotion"]
|
|
migration_path = ROOT / "docs" / "migration-v2.md"
|
|
if promotion:
|
|
promotion_summary = promotion.get("summary", {})
|
|
blocked = int(promotion_summary.get("blocked", 0) or 0)
|
|
release_status = "block" if blocked else "pass"
|
|
release_detail = f"{promotion_summary.get('promote', 0)} promote; {promotion_summary.get('keep_current', 0)} keep current; {blocked} blocked"
|
|
else:
|
|
release_status = "warn"
|
|
release_detail = "promotion decisions are missing; release notes need reviewer confirmation"
|
|
upgrade = data["upgrade_check"]
|
|
if upgrade:
|
|
upgrade_summary = upgrade.get("summary", {})
|
|
if upgrade.get("failures"):
|
|
release_status = "block"
|
|
elif upgrade.get("warnings") and release_status == "pass":
|
|
release_status = "warn"
|
|
release_detail += (
|
|
f"; upgrade {upgrade_summary.get('declared_bump', 'n/a')}"
|
|
f" declared / {upgrade_summary.get('recommended_bump', 'n/a')} recommended"
|
|
)
|
|
gates.append(
|
|
gate(
|
|
"release-notes",
|
|
"发布路线",
|
|
release_status,
|
|
release_detail,
|
|
"reports/promotion_decisions.json + reports/upgrade_check.json + docs/migration-v2.md",
|
|
report_link(output_html, skill_dir, "reports/promotion_decisions.md") if promotion else str(migration_path),
|
|
)
|
|
)
|
|
|
|
return gates
|