"""Pure publication policy and execution presentation; no native graph interpretation.""" from core.dify_builder.contract import TestResultCard from core.dify_builder.models import PublicationDecision, Run from runtime_contracts.builder.copy_ids import BuilderCopyId from runtime_contracts.builder.presentation import copy_text, text_parts from services.dify_builder.effect_presentation import mocked_note from services.dify_builder.execution_messages import FALLBACK, execution_refusal_message from services.dify_builder.json_execution_presentation import json_execution_blocks_repair, json_failed_reason REVIEW_NOTE = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE_NOT_VERIFIED) SUCCESS_REPLY = copy_text(BuilderCopyId.LABELS_EXECUTION_SUCCEEDED_OUTPUT_NEEDS_REVIEW) def is_execution_policy_blocker(run: Run) -> bool: """Known policy denials and incomplete execution proof cannot enter repair.""" if run.execution_refusal is not None: return True summary = run.verification.execution_evidence if run.verification else None if summary is None: return run.kind == "verify" return ( bool(summary.blocked_node_ids) or not summary.sealed or json_execution_blocks_repair(summary, run.status) or summary.safety_outcome in { "unsupported_safe_execution", "execution_blocked", "execution_evidence_unknown", "simulation_completed", "preparation_failed", } ) def publication_decision( run: Run | None, *, session_id: str, current_revision: str, current_graph_revision: str ) -> PublicationDecision: if run is None or run.session_id != session_id or run.kind != "verify" or not run.immutable: return PublicationDecision(allowed=False, reason="no_verified_run") summary = run.verification.execution_evidence if run.verification else None if run.execution_refusal is not None or (summary and summary.blocked_node_ids): return PublicationDecision(allowed=False, reason="execution_policy_blocked") if summary is None or not summary.sealed or not summary.native_run_id or summary.native_run_id != run.dify_run_id: return PublicationDecision(allowed=False, reason="execution_provenance_unbound") if summary.safety_outcome in {"unsupported_safe_execution", "execution_blocked"}: return PublicationDecision(allowed=False, reason="execution_policy_blocked") if summary.safety_outcome == "execution_evidence_unknown": return PublicationDecision(allowed=False, reason="execution_evidence_unknown") if summary.safety_outcome == "simulation_completed": return PublicationDecision(allowed=False, reason="simulation_only") # PRD ยง19.2: a run that mocked any step proves nothing about that step. if summary.mode == "confirmed_run" and summary.simulated_node_ids: return PublicationDecision(allowed=False, reason="simulation_only") restricted_success = summary.safety_outcome == "restricted_execution_completed" and summary.mode == "restricted" confirmed_run_success = ( summary.safety_outcome == "confirmed_run_completed" and summary.mode == "confirmed_run" and summary.policy_version == "builder-confirmed-run-v1" ) # Confirmation is not publication approval: only the sealed exact-revision # confirmed success passes, and every check below still applies. confirmed_success = ( summary.safety_outcome == "confirmed_tool_completed" and summary.mode == "confirmed_tool" and summary.policy_version == "builder-confirmed-gmail-read-v1" and summary.action_state == "completed" and summary.consent_id is not None ) if not (restricted_success or confirmed_run_success or confirmed_success): return PublicationDecision(allowed=False, reason="execution_failed") if run.status == "failed": return PublicationDecision(allowed=False, reason="execution_failed") if run.status != "succeeded": return PublicationDecision(allowed=False, reason="execution_unknown") evidence = run.verification if ( evidence is None or not evidence.execution_revision or not evidence.executed_graph_revision or not current_revision or not current_graph_revision ): return PublicationDecision(allowed=False, reason="revision_unbound") if evidence.execution_revision != current_revision or evidence.executed_graph_revision != current_graph_revision: return PublicationDecision(allowed=False, reason="stale_revision") if any(f.state == "unresolved" for f in evidence.output_findings): return PublicationDecision(allowed=False, reason="required_output_unresolved") if evidence.no_output_dead_branch: return PublicationDecision(allowed=False, reason="dead_branch_without_output") return PublicationDecision(allowed=True, reason="eligible") def has_output_blocker(run: Run) -> bool: evidence = run.verification return evidence is not None and ( evidence.no_output_dead_branch or any(f.state == "unresolved" for f in evidence.output_findings) ) CONFIRMED_COMPLETED_NOTE = copy_text(BuilderCopyId.CONFIRMED_COMPLETED_NOTE) CONFIRMED_FAILED_REASON = copy_text(BuilderCopyId.CONFIRMED_FAILED) CONFIRMED_UNKNOWN_REASON = copy_text(BuilderCopyId.CONFIRMED_UNKNOWN) def _confirmed_result_card(run: Run) -> TestResultCard: """IDs, statuses, counts, reason codes and the Run link only; never native content or error text.""" evidence = run.verification summary = evidence.execution_evidence if evidence else None assert summary is not None completed = summary.safety_outcome == "confirmed_tool_completed" and run.status == "succeeded" card = TestResultCard( status="succeeded" if completed else "failed", dify_run_id=run.dify_run_id, review_note=CONFIRMED_COMPLETED_NOTE, terminal_outputs=None, executed_node_ids=list(evidence.executed_node_ids) if evidence else [], execution_mode=summary.mode, safety_outcome=summary.safety_outcome, policy_version=summary.policy_version, consent_id=summary.consent_id, action_state=summary.action_state, action_reason_code=summary.action_reason_code, ) if completed and has_output_blocker(run): card.status = "failed" card.outcome = "required_output_unresolved" card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED) elif completed: card.outcome = "execution_succeeded_needs_review" elif summary.safety_outcome == "native_failed": card.outcome = "execution_failed" card.failure_reason = CONFIRMED_FAILED_REASON else: card.outcome = "execution_unknown" card.failure_reason = CONFIRMED_UNKNOWN_REASON return card def _project_acceptance(run: Run, card: TestResultCard) -> TestResultCard: """Project typed business state independently of execution and localized prose.""" finding = run.verification.business_acceptance if run.verification else {} if finding: from services.dify_builder.business_acceptance import CaseAcceptance try: result = CaseAcceptance.model_validate(finding) except ValueError: card.business_acceptance_state = "unverified" else: card.business_acceptance_state = result.state card.business_acceptance_case_id = result.case_id return card def result_card(run: Run, *, reply_language: str = "en-US") -> TestResultCard: evidence = run.verification summary = evidence.execution_evidence if evidence else None if summary is not None and summary.mode == "confirmed_tool": return _project_acceptance(run, _confirmed_result_card(run)) card = TestResultCard( status="succeeded" if run.status == "succeeded" else "failed", dify_run_id=run.dify_run_id, review_note=REVIEW_NOTE, terminal_outputs=evidence.terminal_outputs if evidence else None, executed_node_ids=evidence.executed_node_ids if evidence else [], ) _project_acceptance(run, card) summary = evidence.execution_evidence if evidence else None if summary: card.execution_mode = summary.mode card.safety_outcome = summary.safety_outcome card.policy_version = summary.policy_version card.simulated_node_ids = list(summary.simulated_node_ids) card.blocked_node_ids = list(summary.blocked_node_ids) elif run.execution_refusal: card.safety_outcome = run.execution_refusal.safety_outcome else: card.safety_outcome = "execution_evidence_unknown" from services.dify_builder.execution_messages import observed_failure_message observed_failure = observed_failure_message(summary, reply_language=reply_language) if observed_failure is not None: card.outcome = ( "execution_failed" if summary and summary.safety_outcome in {"preparation_failed", "native_failed"} else "execution_unknown" ) card.failure_reason = observed_failure card.failure_phase = summary.failure_phase if summary else None card.failure_code = summary.failure_reason if summary else None card.failure_resource = ( "/".join(filter(None, summary.failure_resource[1:])) if summary and summary.failure_resource else None ) card.failure_node_ids = list(summary.failure_node_ids) if summary else [] card.terminal_outputs = None return card if is_execution_policy_blocker(run): failed_reason = json_failed_reason(summary, run.status) card.outcome = "execution_failed" if failed_reason else "execution_unknown" card.failure_reason = ( execution_refusal_message(run.execution_refusal.reason_code) if run.execution_refusal else failed_reason or FALLBACK ) if failed_reason: card.terminal_outputs = None if run.execution_refusal is not None: card.terminal_outputs = None if not run.dify_run_id: card.executed_node_ids = [] return card if run.status == "succeeded" and has_output_blocker(run): card.status = "failed" card.outcome = "required_output_unresolved" card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED) elif run.status == "succeeded": card.outcome = "execution_succeeded_needs_review" if evidence is None or not evidence.execution_revision: card.review_note = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE) mocked = ( mocked_note(summary.simulated_node_ids, run.per_node) if summary is not None and summary.mode == "confirmed_run" else None ) if mocked is not None: card.review_note = text_parts(card.review_note, " ", mocked) if card.review_note else mocked elif run.status == "failed": card.outcome = "execution_failed" card.failure_reason = run.error or next( (n.error for n in run.per_node if n.error), copy_text(BuilderCopyId.LABELS_EXECUTION_FAILED) ) else: card.outcome = "execution_unknown" card.failure_reason = run.error or copy_text( BuilderCopyId.REPLIES_EXECUTION_OUTCOME_IS_UNKNOWN_RUN_AGAIN_BEFORE_PUBLISHING ) return card