From c6215f194c5f71c85c405cb33c3ae5ed83d581f5 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Thu, 7 May 2026 00:36:17 +0200 Subject: [PATCH] Prefer stronger analytics on tied template launches --- .../HyperTwistCoachDashboardWidget.cpp | 117 ++++++++++++++++++ ..._ANALYTICS_TIEBREAKER_PACKET_2026-05-07.md | 91 ++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_TIEBREAKER_PACKET_2026-05-07.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index ee5577d..fd2a72d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -433,6 +433,96 @@ namespace HyperTwistCoachDashboardWidgetInternal return Score; } + FString ResolveDashboardTemplateAnalyticsUserId( + const FHyperTwistTrainingSessionTemplate& Template, + const FString& DefaultUserId + ) + { + return !Template.UserId.IsEmpty() ? Template.UserId : DefaultUserId; + } + + int32 CountTemplateLaunchOutcomeRuns(const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats) + { + return TemplateStats != nullptr && TemplateStats->IsStructurallyValid() + ? (TemplateStats->CompletedRunCount + TemplateStats->AbortedRunCount) + : 0; + } + + int32 BuildTemplateLaunchAnalyticsTieBreakScore( + const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats + ) + { + if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid()) + { + return 0; + } + + const int32 OutcomeRuns = CountTemplateLaunchOutcomeRuns(TemplateStats); + if (OutcomeRuns <= 0) + { + return 0; + } + + int32 Score = 0; + Score += FMath::Clamp(OutcomeRuns, 0, 4); + Score += FMath::Clamp(FMath::RoundToInt(TemplateStats->OverallSuccessRate * 10.0f), 0, 10); + Score -= FMath::Clamp(FMath::RoundToInt(TemplateStats->AverageMistakesPerOutcomeRun * 2.0f), 0, 8); + Score -= FMath::Clamp(TemplateStats->AbortedRunCount, 0, 3); + return Score; + } + + bool ShouldPreferTemplateByAnalytics( + const FHyperTwistTrainingTemplateLaunchScopedStats* CandidateStats, + const FHyperTwistTrainingTemplateLaunchScopedStats* IncumbentStats + ) + { + const int32 CandidateOutcomeRuns = CountTemplateLaunchOutcomeRuns(CandidateStats); + const int32 IncumbentOutcomeRuns = CountTemplateLaunchOutcomeRuns(IncumbentStats); + if (CandidateOutcomeRuns < 2 && IncumbentOutcomeRuns < 2) + { + return false; + } + + const int32 CandidateScore = BuildTemplateLaunchAnalyticsTieBreakScore(CandidateStats); + const int32 IncumbentScore = BuildTemplateLaunchAnalyticsTieBreakScore(IncumbentStats); + if (CandidateScore != IncumbentScore) + { + return CandidateScore > IncumbentScore; + } + if (CandidateOutcomeRuns != IncumbentOutcomeRuns) + { + return CandidateOutcomeRuns > IncumbentOutcomeRuns; + } + + const float CandidateSuccessRate = + CandidateStats != nullptr && CandidateStats->IsStructurallyValid() + ? CandidateStats->OverallSuccessRate + : 0.0f; + const float IncumbentSuccessRate = + IncumbentStats != nullptr && IncumbentStats->IsStructurallyValid() + ? IncumbentStats->OverallSuccessRate + : 0.0f; + if (FMath::Abs(CandidateSuccessRate - IncumbentSuccessRate) >= 0.05f) + { + return CandidateSuccessRate > IncumbentSuccessRate; + } + + const float CandidateMistakeRate = + CandidateStats != nullptr && CandidateStats->IsStructurallyValid() + ? CandidateStats->AverageMistakesPerOutcomeRun + : 0.0f; + const float IncumbentMistakeRate = + IncumbentStats != nullptr && IncumbentStats->IsStructurallyValid() + ? IncumbentStats->AverageMistakesPerOutcomeRun + : 0.0f; + if (FMath::Abs(CandidateMistakeRate - IncumbentMistakeRate) >= 0.5f) + { + return CandidateMistakeRate < IncumbentMistakeRate; + } + + return false; + } + EMethodDrillRecoveryMemoryBias ResolveMethodDrillRecoveryMemoryBias( const int32 HistoryCount, const int32 StrongCount, @@ -7036,6 +7126,33 @@ const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindP { PreferredTemplate = &SessionTemplate; PreferredScore = TemplateScore; + continue; + } + + if (TemplateScore == PreferredScore + && TemplateScore > 0 + && PreferredTemplate != nullptr) + { + const FString CandidateUserId = + HyperTwistCoachDashboardWidgetInternal::ResolveDashboardTemplateAnalyticsUserId( + SessionTemplate, + DefaultUserId + ); + const FString PreferredUserId = + HyperTwistCoachDashboardWidgetInternal::ResolveDashboardTemplateAnalyticsUserId( + *PreferredTemplate, + DefaultUserId + ); + const FHyperTwistTrainingTemplateLaunchScopedStats* CandidateStats = + FindCachedTemplateLaunchScopedStats(SessionTemplate.TemplateId, CandidateUserId); + const FHyperTwistTrainingTemplateLaunchScopedStats* PreferredStats = + FindCachedTemplateLaunchScopedStats(PreferredTemplate->TemplateId, PreferredUserId); + if (HyperTwistCoachDashboardWidgetInternal::ShouldPreferTemplateByAnalytics( + CandidateStats, + PreferredStats)) + { + PreferredTemplate = &SessionTemplate; + } } } diff --git a/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_TIEBREAKER_PACKET_2026-05-07.md b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_TIEBREAKER_PACKET_2026-05-07.md new file mode 100644 index 0000000..7747b82 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_TIEBREAKER_PACKET_2026-05-07.md @@ -0,0 +1,91 @@ +# HyperTwist Phase 4 template launch analytics tie-breaker packet + +Created on `2026-05-07` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` dashboard template selection slice + +## Purpose + +This packet adds a conservative analytics tie-breaker to dashboard template launch selection. + +The open tasks were: + +- keep the existing review and recovery template priority rules intact +- stop breaking equal-priority template ties by incidental cache order alone +- prefer the equally eligible template with the stronger stored outcome signal when enough history exists + +It is not: + +- a primary template ranking rewrite packet +- a change to review-plan resume semantics +- a change to template provenance persistence +- a broader coach weighting or recommendation packet + +## Scope + +Bounded lane: + +- add a small dashboard helper for resolving the analytics user id for a template +- derive a conservative tie-break score from cached per-template scoped stats +- require enough stored outcome history before analytics can affect selection +- apply the tie-break only when two active templates already have the same dashboard launch priority score + +Out of scope: + +- changing the existing review or recovery template score bands +- changing repository aggregation logic for template launch stats +- introducing new UI surfaces or controls +- using template analytics outside the dashboard launch selector + +## Why this was the right next packet + +The previous packet surfaced repo-backed per-template outcome stats on the dashboard. + +That still left one clean next gap: + +- operators could now see the per-template outcome signal +- but the actual preferred template selector still ignored that signal +- and equal-priority templates were still effectively chosen by iteration order + +So the next honest move was: + +- keep the current launch scoring rules as the primary decision +- add analytics only as a tie-breaker between already-equal candidates +- and gate that tie-break behind a modest history threshold so one noisy run does not distort launch selection + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - added helper logic for resolving template analytics ownership, counting outcome runs, and building a bounded tie-break score from stored success, mistake, and abort signals + - added a conservative analytics preference check that only activates when at least one tied template has meaningful stored outcome history + - updated `FindPreferredDashboardTemplateLaunch()` so equally scored dashboard template candidates use the analytics tie-break instead of falling through to cache order + +## Product effect + +Preferred dashboard template launch selection is now slightly smarter without changing the visible priority model: + +- review and recovery templates still win or lose on the same primary score rules as before +- analytics only matter when templates are already equally eligible +- templates with better stored success and lower mistake pressure now win tied selections more often +- templates with too little history still behave as before, which keeps the selector stable under sparse data + +## Acceptance criteria + +- dashboard template selection still uses existing primary score rules +- analytics only influence selection when candidates tie on primary score +- sparse history does not trigger analytics-driven template churn +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm preferred dashboard template selection still respects the existing review and recovery priority order +3. confirm tied templates with stronger stored success and lower mistake pressure are preferred when scoped stats exist +4. confirm templates with insufficient outcome history still fall back to the preexisting selector behavior + +That is the packet.