From a601bc46a98b62faebf21a33c94f0f55b9ca7f07 Mon Sep 17 00:00:00 2001 From: yuqei Date: Tue, 6 Oct 2026 13:06:11 +0800 Subject: [PATCH] fix(scene): bind release gate to current readiness --- .../yuqei_contract_api/drafting_workspace.py | 3 + .../template_evaluation_evidence.py | 17 ++++ .../tests/test_template_evaluations.py | 91 ++++++++++++++++++- .../tests/test_template_release_readiness.py | 2 + 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/drafting_workspace.py b/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/drafting_workspace.py index 46252e04..63b34fb4 100644 --- a/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/drafting_workspace.py +++ b/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/drafting_workspace.py @@ -681,6 +681,9 @@ class DraftingWorkspaceStore: summary.update({ "source_applicability_verified": readiness.get("source_applicability_verified") is True, "published_prompt_alignment_verified": readiness.get("published_prompt_alignment_verified") is True, + # This is derived from the complete server-side readiness check. + # No edit, runtime, or API request field is accepted as a shortcut. + "release_readiness_prerequisites_satisfied": readiness.get("prerequisites_satisfied") is True, "release_readiness": readiness, }) return summary diff --git a/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_evaluation_evidence.py b/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_evaluation_evidence.py index 900529c4..d6e43369 100644 --- a/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_evaluation_evidence.py +++ b/yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_evaluation_evidence.py @@ -30,6 +30,8 @@ def scene_template_release_gate( ) -> dict: """Recompute release readiness from bound evidence, never cached approval flags.""" evidence = model_evidence if isinstance(model_evidence, dict) else {} + readiness = evidence.get("release_readiness") + readiness = readiness if isinstance(readiness, dict) else {} blockers: list[dict[str, str]] = [] def block(code: str, message: str) -> None: @@ -89,6 +91,19 @@ def scene_template_release_gate( block("RELEASE_SOURCE_APPLICABILITY", "场景法源适用性尚未形成独立可核验结论。") if evidence.get("published_prompt_alignment_verified") is not True: block("RELEASE_RUNTIME_PROMPT_ALIGNMENT", "评测 Prompt 与拟发布运行时 Prompt 尚未完成哈希对齐。") + readiness_prerequisites_satisfied = ( + evidence.get("release_readiness_prerequisites_satisfied") is True + and readiness.get("schema_version") == "scene-release-readiness.v1" + and readiness.get("prerequisites_satisfied") is True + and readiness.get("issues") == [] + and readiness.get("source_applicability_verified") is True + and readiness.get("published_prompt_alignment_verified") is True + and readiness.get("missing_draft_variants") == [] + and readiness.get("missing_review_variants") == [] + and readiness.get("missing_holdout_draft_variants") == [] + ) + if not readiness_prerequisites_satisfied: + block("RELEASE_READINESS_PREREQUISITES", "服务端当前发布就绪条件尚未全部满足,历史 Prompt 覆盖不能替代当前运行证据。") bound_payload = { "policy_version": RELEASE_GATE_POLICY_VERSION, @@ -103,6 +118,8 @@ def scene_template_release_gate( for group in complete_review], "source_applicability_verified": evidence.get("source_applicability_verified") is True, "published_prompt_alignment_verified": evidence.get("published_prompt_alignment_verified") is True, + "release_readiness_evidence_sha256": draft_digest(readiness) if readiness else None, + "release_readiness_prerequisites_satisfied": readiness_prerequisites_satisfied, "blocker_codes": [item["code"] for item in blockers], } return { diff --git a/yuqei-contract-platform/services/contract-api/tests/test_template_evaluations.py b/yuqei-contract-platform/services/contract-api/tests/test_template_evaluations.py index 19f7f0e7..4daafb4a 100644 --- a/yuqei-contract-platform/services/contract-api/tests/test_template_evaluations.py +++ b/yuqei-contract-platform/services/contract-api/tests/test_template_evaluations.py @@ -463,7 +463,9 @@ def test_scene_release_gate_recomputes_all_evidence_and_ignores_cached_approval_ ) assert passed["policy_version"] == RELEASE_GATE_POLICY_VERSION assert not passed["passed"] and not passed["authorizes_publication"] - assert {item["code"] for item in passed["blockers"]} == {"RELEASE_HOLDOUT_EVIDENCE_MISSING"} + assert {item["code"] for item in passed["blockers"]} == { + "RELEASE_HOLDOUT_EVIDENCE_MISSING", "RELEASE_READINESS_PREREQUISITES", + } assert len(passed["evidence_sha256"]) == 64 stale = scene_template_release_gate( @@ -477,6 +479,93 @@ def test_scene_release_gate_recomputes_all_evidence_and_ignores_cached_approval_ }) +def test_final_publication_validation_rejects_complete_coverage_from_stale_prompts(monkeypatch): + from yuqei_contract_api.template_evaluation_evidence import ( + POLICY_VERSION, + VARIANTS, + deterministic_scene_evidence, + scene_template_release_gate, + ) + + monkeypatch.setattr( + "yuqei_contract_api.holdout_scene_evidence_registry.verify_scene_holdout_evidence", + lambda evidence, *, current_template_content_sha256: True, + ) + repository = InMemoryContractRepository(contracts=[]) + template = repository.get_template(SCENE) + draft = repository.get_template_edit_draft(SCENE) + deterministic = deterministic_scene_evidence(template, draft.body_content) + stale_draft_prompt = { + "name": "contract_drafting_v1", + "version": "stale-version", + "prompt_sha256": "c" * 64, + } + stale_review_prompt = { + "name": "contract_review_transport", + "version": "stale-version", + "content_sha256": "d" * 64, + } + stale_readiness = { + "schema_version": "scene-release-readiness.v1", + "issues": [ + {"code": "SCENE_DRAFT_COVERAGE_PENDING", "message": "旧 Prompt 起草覆盖不计入当前运行版本。"}, + {"code": "SCENE_REVIEW_COVERAGE_PENDING", "message": "旧 Prompt 审查覆盖不计入当前运行版本。"}, + ], + "source_applicability_verified": True, + "published_prompt_alignment_verified": True, + "missing_draft_variants": list(VARIANTS), + "missing_review_variants": list(VARIANTS), + "missing_holdout_draft_variants": [], + "prerequisites_satisfied": False, + } + evidence = { + "policy_version": POLICY_VERSION, + "template_content_sha256": deterministic["template_content_sha256"], + "current_records": len(VARIANTS), + "failed_or_unverified_records": 0, + "truncated": False, + "coverage_by_prompt": [{ + "prompt": stale_draft_prompt, + "passed_variants": list(VARIANTS), + "missing_variants": [], + }], + "review_coverage_by_prompt": [{ + "draft_prompt": stale_draft_prompt, + "prompt": stale_review_prompt, + "passed_variants": list(VARIANTS), + "missing_variants": [], + }], + "holdout_registry_evidence": { + "evidence_sha256": "e" * 64, + "current_template_match": True, + "missing_or_failed_variants": [], + }, + "source_applicability_verified": True, + "published_prompt_alignment_verified": True, + # A cached or forged top-level claim cannot override server readiness. + "release_readiness_prerequisites_satisfied": True, + "release_readiness": stale_readiness, + "authorizes_publication": True, + } + + gate = scene_template_release_gate( + template_content_sha256=deterministic["template_content_sha256"], + deterministic=deterministic, + model_evidence=evidence, + ) + assert {item["code"] for item in gate["blockers"]} == {"RELEASE_READINESS_PREREQUISITES"} + assert gate["release_readiness_prerequisites_satisfied"] is False + assert not gate["passed"] and not gate["authorizes_publication"] + + repository.get_template_evaluation_summary = lambda template_id: deepcopy(evidence) + validation = repository.validate_template_before_publish(SCENE) + assert not validation.valid + assert validation.evaluation_summary["release_gate"]["blocker_codes"] == [ + "RELEASE_READINESS_PREREQUISITES" + ] + assert "SCENE_EVALUATION_PENDING" in {issue.code for issue in validation.issues} + + def test_evidence_recomputed_against_payload_not_cached_flags(stores): _, trial = stores workspace_id, _, _ = attach_stubbed_evaluation(trial) diff --git a/yuqei-contract-platform/services/contract-api/tests/test_template_release_readiness.py b/yuqei-contract-platform/services/contract-api/tests/test_template_release_readiness.py index 66990c56..83123d10 100644 --- a/yuqei-contract-platform/services/contract-api/tests/test_template_release_readiness.py +++ b/yuqei-contract-platform/services/contract-api/tests/test_template_release_readiness.py @@ -106,6 +106,7 @@ def test_summary_top_level_release_flags_follow_only_verified_server_readiness(m summary = trial.template_evaluation_summary(SCENE, tenant="default") assert summary["source_applicability_verified"] is True assert summary["published_prompt_alignment_verified"] is True + assert summary["release_readiness_prerequisites_satisfied"] is summary["release_readiness"]["prerequisites_satisfied"] assert summary["source_applicability_verified"] is summary["release_readiness"]["source_applicability_verified"] assert summary["published_prompt_alignment_verified"] is summary["release_readiness"]["published_prompt_alignment_verified"] @@ -122,6 +123,7 @@ def test_summary_top_level_release_flags_reject_cached_claims_and_bad_runtime_ha summary = trial.template_evaluation_summary(SCENE, tenant="default") assert summary["source_applicability_verified"] is False assert summary["published_prompt_alignment_verified"] is False + assert summary["release_readiness_prerequisites_satisfied"] is False assert "SCENE_RUNTIME_UNAVAILABLE" in codes(summary["release_readiness"])