diff --git a/src/ui/main_window_parts/persona.py b/src/ui/main_window_parts/persona.py index bf36f53..172d961 100644 --- a/src/ui/main_window_parts/persona.py +++ b/src/ui/main_window_parts/persona.py @@ -486,6 +486,24 @@ def _on_persona_creation_completed( or "" ) + friendly_title = str( + getattr( + result, + "friendly_title", + "", + ) + or "" + ) + + friendly_message = str( + getattr( + result, + "friendly_message", + "", + ) + or "" + ) + if self.persona_dialog is not None: if readiness_ready: final_message = ( @@ -493,6 +511,11 @@ def _on_persona_creation_completed( f"{', '.join(completed_modes)}. " f"Runtime READY: {recommended_policy}" ) + elif friendly_message: + final_message = ( + f"{friendly_title}\n" + f"{friendly_message}" + ) else: final_message = ( f"Persona {username} selesai: " diff --git a/src/ui/persona_creation_worker.py b/src/ui/persona_creation_worker.py index 1533eda..c608558 100644 --- a/src/ui/persona_creation_worker.py +++ b/src/ui/persona_creation_worker.py @@ -17,6 +17,7 @@ build_unified_full_pipeline_command, complete_persona_exists, disk_space_status_for_pipeline, + friendly_pipeline_status, normalize_pipeline_time_class, normalize_pipeline_username, progress_percent_for_pipeline_line, @@ -116,7 +117,7 @@ def _run_subprocess( raise PersonaBuildCancelled() self._recent_lines.append(line) - self._recent_lines = self._recent_lines[-40:] + self._recent_lines = self._recent_lines[-80:] self._report_progress( progress_percent_for_pipeline_line(line), @@ -149,12 +150,17 @@ def _build_readiness_result( line, ) - warning = "" - if pipeline_returncode != 0: - warning = ( - "Full pipeline ended with a non-zero exit code, " - "but complete persona output exists; UI will load it " - "and keep runtime policy safe according to readiness." + friendly = friendly_pipeline_status( + pipeline_returncode=pipeline_returncode, + readiness_ready=bool(report.ready), + readiness_report_text=rendered, + recent_output_lines=tuple(self._recent_lines), + ) + + if pipeline_returncode != 0 or not report.ready: + self._report_progress( + 99, + friendly.message, ) return UnifiedPersonaPipelineResult( @@ -166,7 +172,12 @@ def _build_readiness_result( recommended_policy=report.recommendation.move_policy_label, move_policy=report.recommendation.move_policy, readiness_report_text=rendered, - warning=warning, + warning="", + friendly_status=friendly.status, + friendly_title=friendly.title, + friendly_message=friendly.message, + safe_to_play=friendly.safe_to_play, + advanced_ml_ready=friendly.advanced_ml_ready, ) def _disk_full_failure_message(self) -> str: @@ -230,12 +241,6 @@ def run(self) -> None: result = self._build_readiness_result(return_code) - if result.warning: - self._report_progress( - 99, - result.warning, - ) - except PersonaBuildCancelled: self.cancelled.emit() return diff --git a/src/ui/unified_persona_pipeline.py b/src/ui/unified_persona_pipeline.py index e9d2c4c..6e4052a 100644 --- a/src/ui/unified_persona_pipeline.py +++ b/src/ui/unified_persona_pipeline.py @@ -29,6 +29,15 @@ class UnifiedPersonaPipelineConfig: adaptive_direct_games: bool = True +@dataclass(frozen=True) +class PipelineFriendlyStatus: + status: str + title: str + message: str + safe_to_play: bool + advanced_ml_ready: bool + + @dataclass(frozen=True) class UnifiedPersonaPipelineResult: username: str @@ -40,6 +49,11 @@ class UnifiedPersonaPipelineResult: move_policy: str readiness_report_text: str warning: str = "" + friendly_status: str = "" + friendly_title: str = "" + friendly_message: str = "" + safe_to_play: bool = True + advanced_ml_ready: bool = False @dataclass(frozen=True) @@ -251,3 +265,83 @@ def complete_persona_exists( username, time_class, ).exists() + + +def friendly_pipeline_status( + *, + pipeline_returncode: int, + readiness_ready: bool, + readiness_report_text: str = "", + recent_output_lines: Sequence[str] = (), +) -> PipelineFriendlyStatus: + combined = "\n".join( + ( + str(readiness_report_text or ""), + *(str(line) for line in recent_output_lines), + ) + ).lower() + + if readiness_ready: + return PipelineFriendlyStatus( + status="runtime_ready", + title="Persona ready — advanced runtime ready", + message=( + "Persona ready. Advanced ML runtime is ready and the UI " + "can use the recommended policy." + ), + safe_to_play=True, + advanced_ml_ready=True, + ) + + if "needs_more_evidence" in combined: + return PipelineFriendlyStatus( + status="advanced_ml_needs_more_evidence", + title="Persona ready — advanced ML needs more evidence", + message=( + "Persona ready. Advanced ML was trained, but the release " + "gate needs more evidence before it can be enabled. " + "The bot will use Statistical selector safely." + ), + safe_to_play=True, + advanced_ml_ready=False, + ) + + if ( + "elite_evidence_missing" in combined + or "statistical_only" in combined + or "recommended ui policy: statistical selector" in combined + ): + return PipelineFriendlyStatus( + status="persona_ready_statistical", + title="Persona ready — using safe fallback", + message=( + "Persona ready. Advanced runtime is not unlocked yet, " + "so the bot will use Statistical selector safely." + ), + safe_to_play=True, + advanced_ml_ready=False, + ) + + if pipeline_returncode != 0: + return PipelineFriendlyStatus( + status="persona_ready_with_pipeline_warning", + title="Persona ready — pipeline finished with warning", + message=( + "Persona ready, but one advanced pipeline step did not " + "finish cleanly. The bot will use the safe runtime policy " + "selected by readiness checks." + ), + safe_to_play=True, + advanced_ml_ready=False, + ) + + return PipelineFriendlyStatus( + status="persona_ready_statistical", + title="Persona ready — using safe fallback", + message=( + "Persona ready. Runtime readiness did not unlock advanced ML, " + "so the bot will use Statistical selector safely." + ), + safe_to_play=True, + advanced_ml_ready=False, + ) diff --git a/tests/test_ui_unified_persona_pipeline.py b/tests/test_ui_unified_persona_pipeline.py index 72bcd22..c879bcc 100644 --- a/tests/test_ui_unified_persona_pipeline.py +++ b/tests/test_ui_unified_persona_pipeline.py @@ -8,6 +8,7 @@ complete_persona_exists, disk_space_status_for_pipeline, expected_complete_persona_path, + friendly_pipeline_status, normalize_pipeline_time_class, progress_percent_for_pipeline_line, resolve_pipeline_counts, @@ -87,3 +88,32 @@ def test_disk_space_status_for_pipeline_reports_requirement( assert status.required_gb >= 10.0 assert status.free_gb >= 0.0 assert isinstance(status.ok, bool) + + +def test_friendly_pipeline_status_for_needs_more_evidence() -> None: + status = friendly_pipeline_status( + pipeline_returncode=1, + readiness_ready=False, + readiness_report_text="Runtime readiness: NOT READY", + recent_output_lines=( + "Decision : needs_more_evidence", + "Failed checks : temporal.coverage.train_games", + ), + ) + + assert status.status == "advanced_ml_needs_more_evidence" + assert status.safe_to_play is True + assert status.advanced_ml_ready is False + assert "Statistical selector" in status.message + + +def test_friendly_pipeline_status_for_runtime_ready() -> None: + status = friendly_pipeline_status( + pipeline_returncode=0, + readiness_ready=True, + readiness_report_text="Runtime readiness: READY", + ) + + assert status.status == "runtime_ready" + assert status.safe_to_play is True + assert status.advanced_ml_ready is True