diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 7008665..247515f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -113,6 +113,10 @@ namespace HyperTwistCoachDashboardWidgetInternal const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats ); + FString BuildTemplateLaunchAnalyticsTieBreakSupportSplitFragment( + const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats + ); + FString BuildTemplateLaunchAnalyticsDetailFragment( const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats ) @@ -123,7 +127,7 @@ namespace HyperTwistCoachDashboardWidgetInternal } return FString::Printf( - TEXT("avg recommended %d | avg focus %d | avg completed attempts %d | total success/failure/timeout/dnf %d/%d/%d/%d | avg mistakes %.2f | review-backed %d | coach-review %d | short set %d | selector split %s | last launch %s | last completed %s"), + TEXT("avg recommended %d | avg focus %d | avg completed attempts %d | total success/failure/timeout/dnf %d/%d/%d/%d | avg mistakes %.2f | review-backed %d | coach-review %d | short set %d | selector split %s | tie-break support %s | last launch %s | last completed %s"), TemplateStats->AverageRecommendedCaseCount, TemplateStats->AverageFocusCaseCount, TemplateStats->AverageCompletedAttemptCount, @@ -136,6 +140,7 @@ namespace HyperTwistCoachDashboardWidgetInternal TemplateStats->CoachReviewLaunchCount, TemplateStats->ShortReviewSetLaunchCount, *BuildTemplateLaunchSelectionSplitAnalyticsFragment(TemplateStats), + *BuildTemplateLaunchAnalyticsTieBreakSupportSplitFragment(TemplateStats), TemplateStats->LastLaunchAtUtc.IsEmpty() ? TEXT("n/a") : *TemplateStats->LastLaunchAtUtc, @@ -333,6 +338,49 @@ namespace HyperTwistCoachDashboardWidgetInternal return Fragment; } + FString BuildTemplateLaunchAnalyticsTieBreakSupportSplitFragment( + const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats + ) + { + if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid()) + { + return TEXT("no analytics tie-break support split is available for this template id yet"); + } + + const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& TemplateSignalStats = + TemplateStats->DashboardAnalyticsTieBreakTemplateScopedSignalStats; + const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& SplitConfidenceStats = + TemplateStats->DashboardAnalyticsTieBreakSelectorSplitConfidenceStats; + if (!TemplateSignalStats.IsStructurallyValid() && !SplitConfidenceStats.IsStructurallyValid()) + { + return TEXT("no analytics tie-break support split is available for this template id yet"); + } + + FString Fragment = FString::Printf( + TEXT("%s | %s"), + *BuildTemplateLaunchSelectionLaneAnalyticsSummary( + TemplateSignalStats, + TEXT("template-signal")), + *BuildTemplateLaunchSelectionLaneAnalyticsSummary( + SplitConfidenceStats, + TEXT("split-confidence")) + ); + if (TemplateSignalStats.IsStructurallyValid() + && SplitConfidenceStats.IsStructurallyValid() + && TemplateSignalStats.GetOutcomeRunCount() > 0 + && SplitConfidenceStats.GetOutcomeRunCount() > 0) + { + Fragment += FString::Printf( + TEXT(" | delta success %+.2f | delta mistakes %+.2f"), + SplitConfidenceStats.OverallSuccessRate - TemplateSignalStats.OverallSuccessRate, + SplitConfidenceStats.AverageMistakesPerOutcomeRun + - TemplateSignalStats.AverageMistakesPerOutcomeRun + ); + } + + return Fragment; + } + FString BuildTemplateLaunchSelectionAnalyticsSupportDetailFragment( const FHyperTwistTrainingTemplateLaunchSelectionProvenance* SelectionProvenance ) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index f772b81..39eb82f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -9161,6 +9161,8 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser( int32 OutcomeRunSampleCount = 0; FSelectionScopedAccumulator DashboardPrimarySelectionAccumulator; FSelectionScopedAccumulator DashboardAnalyticsTieBreakSelectionAccumulator; + FSelectionScopedAccumulator DashboardAnalyticsTieBreakTemplateScopedSignalAccumulator; + FSelectionScopedAccumulator DashboardAnalyticsTieBreakSelectorSplitConfidenceAccumulator; }; const FString ResolvedReferenceUtc = @@ -9261,6 +9263,8 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser( { SelectionAccumulator.Stats.SelectorSourceLabel = SelectionProvenance.SelectorSourceLabel; SelectionAccumulator.Stats.ReasonClass = SelectionProvenance.ReasonClass; + SelectionAccumulator.Stats.AnalyticsSupportKind = + SelectionProvenance.AnalyticsSupportKind; SelectionAccumulator.Stats.LaunchCount += 1; SelectionAccumulator.Stats.TotalCompletedAttemptCount += RunRecord.Summary.CompletedAttemptCount; @@ -9304,6 +9308,22 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser( Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator, Provenance.SelectionProvenance ); + if (Provenance.SelectionProvenance.AnalyticsSupportKind == + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind::TemplateScopedOutcomeSignal) + { + AccumulateSelectionScopedStats( + Accumulator.DashboardAnalyticsTieBreakTemplateScopedSignalAccumulator, + Provenance.SelectionProvenance + ); + } + else if (Provenance.SelectionProvenance.AnalyticsSupportKind == + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind::SelectorSplitConfidence) + { + AccumulateSelectionScopedStats( + Accumulator.DashboardAnalyticsTieBreakSelectorSplitConfidenceAccumulator, + Provenance.SelectionProvenance + ); + } } } @@ -9363,37 +9383,44 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser( ? static_cast(Accumulator.MistakeAccumulator) / static_cast(Accumulator.OutcomeRunSampleCount) : 0.0f; - Accumulator.DashboardPrimarySelectionAccumulator.Stats.OverallSuccessRate = - Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalCompletedAttemptCount > 0 - ? static_cast( - Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalSuccessCount) - / static_cast( - Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalCompletedAttemptCount) - : 0.0f; - Accumulator.DashboardPrimarySelectionAccumulator.Stats.AverageMistakesPerOutcomeRun = - Accumulator.DashboardPrimarySelectionAccumulator.OutcomeRunSampleCount > 0 - ? static_cast(Accumulator.DashboardPrimarySelectionAccumulator.MistakeAccumulator) - / static_cast( - Accumulator.DashboardPrimarySelectionAccumulator.OutcomeRunSampleCount) - : 0.0f; - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.OverallSuccessRate = - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalCompletedAttemptCount > 0 - ? static_cast( - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalSuccessCount) - / static_cast( - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalCompletedAttemptCount) - : 0.0f; - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.AverageMistakesPerOutcomeRun = - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.OutcomeRunSampleCount > 0 - ? static_cast( - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.MistakeAccumulator) - / static_cast( - Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.OutcomeRunSampleCount) - : 0.0f; + auto FinalizeSelectionScopedAccumulator = + []( + FTemplateLaunchScopedStatsAccumulator::FSelectionScopedAccumulator& SelectionAccumulator) + { + SelectionAccumulator.Stats.OverallSuccessRate = + SelectionAccumulator.Stats.TotalCompletedAttemptCount > 0 + ? static_cast(SelectionAccumulator.Stats.TotalSuccessCount) + / static_cast( + SelectionAccumulator.Stats.TotalCompletedAttemptCount) + : 0.0f; + SelectionAccumulator.Stats.AverageMistakesPerOutcomeRun = + SelectionAccumulator.OutcomeRunSampleCount > 0 + ? static_cast(SelectionAccumulator.MistakeAccumulator) + / static_cast(SelectionAccumulator.OutcomeRunSampleCount) + : 0.0f; + }; + FinalizeSelectionScopedAccumulator(Accumulator.DashboardPrimarySelectionAccumulator); + FinalizeSelectionScopedAccumulator( + Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator + ); + Accumulator.DashboardPrimarySelectionAccumulator.Stats.AnalyticsSupportKind = + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind::None; + Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.AnalyticsSupportKind = + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind::None; + FinalizeSelectionScopedAccumulator( + Accumulator.DashboardAnalyticsTieBreakTemplateScopedSignalAccumulator + ); + FinalizeSelectionScopedAccumulator( + Accumulator.DashboardAnalyticsTieBreakSelectorSplitConfidenceAccumulator + ); Accumulator.Stats.DashboardPrimarySelectionStats = Accumulator.DashboardPrimarySelectionAccumulator.Stats; Accumulator.Stats.DashboardAnalyticsTieBreakStats = Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats; + Accumulator.Stats.DashboardAnalyticsTieBreakTemplateScopedSignalStats = + Accumulator.DashboardAnalyticsTieBreakTemplateScopedSignalAccumulator.Stats; + Accumulator.Stats.DashboardAnalyticsTieBreakSelectorSplitConfidenceStats = + Accumulator.DashboardAnalyticsTieBreakSelectorSplitConfidenceAccumulator.Stats; if (Accumulator.Stats.IsStructurallyValid()) { ScopedStats.Add(Accumulator.Stats); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 0adff60..0fe8531 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2037,6 +2037,10 @@ struct FHyperTwistTrainingTemplateLaunchSelectionScopedStats EHyperTwistTrainingTemplateLaunchSelectionReasonClass ReasonClass = EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind AnalyticsSupportKind = + EHyperTwistTrainingTemplateLaunchSelectionAnalyticsSupportKind::None; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 LaunchCount = 0; @@ -2173,6 +2177,14 @@ struct FHyperTwistTrainingTemplateLaunchScopedStats UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistTrainingTemplateLaunchSelectionScopedStats DashboardAnalyticsTieBreakStats; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingTemplateLaunchSelectionScopedStats + DashboardAnalyticsTieBreakTemplateScopedSignalStats; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingTemplateLaunchSelectionScopedStats + DashboardAnalyticsTieBreakSelectorSplitConfidenceStats; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString LastLaunchAtUtc; diff --git a/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_SUPPORT_SPLIT_PACKET_2026-05-07.md b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_SUPPORT_SPLIT_PACKET_2026-05-07.md new file mode 100644 index 0000000..8040315 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_ANALYTICS_SUPPORT_SPLIT_PACKET_2026-05-07.md @@ -0,0 +1,96 @@ +# HyperTwist Phase 4 template launch analytics support split packet + +Created on `2026-05-07` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` dashboard template analytics refinement slice + +## Purpose + +This packet aggregates analytics tie-break outcomes by persisted support kind so the dashboard can compare tie-break launches backed by selector-split confidence against tie-break launches decided by broader template-scoped outcome signal alone. + +The open tasks were: + +- stop leaving the new analytics support provenance as run-by-run audit detail only +- derive repo-backed per-template support-kind outcome splits inside the existing template launch analytics lane +- surface that support-kind comparison back into the dashboard without creating a new screen + +It is not: + +- another selector math packet +- a new template launch history packet +- a new repository API surface beyond scoped stat derivation +- a non-dashboard analytics consumer packet + +## Scope + +Bounded lane: + +- extend selection-scoped template stats to carry analytics support identity +- derive per-template analytics-tie-break support-kind aggregates from stored run provenance +- persist two repo-backed support buckets for dashboard analytics tie-break launches: broader template-scoped signal and selector-split confidence +- append the new support-kind split comparison to the shared dashboard template analytics detail fragment already used by live launch, template history, and recap + +Out of scope: + +- changing the current analytics support provenance schema +- changing selector confidence thresholds +- feeding the new support-kind split back into selector behavior +- adding separate support-kind navigation or controls + +## Why this was the right next packet + +The previous packet made it possible to audit whether a single analytics tie-break win carried selector-split confidence support. + +That still left the obvious aggregate gap: + +- operators could inspect support kind on an individual launch +- but they still could not tell whether selector-split-confidence-backed tie-breaks were actually outperforming broader template-signal tie-breaks over time +- and the repo-backed template analytics lane still treated all analytics tie-break launches as one bucket + +So the next honest move was: + +- keep the existing repo-backed template launch scoped stats as the aggregation home +- split analytics tie-break outcomes by support kind inside that same structure +- and surface that comparison through the dashboard’s existing shared analytics detail path + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added analytics-support identity to `FHyperTwistTrainingTemplateLaunchSelectionScopedStats` + - added per-template support-kind analytics tie-break stat buckets for template-scoped signal and selector-split confidence +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp` + - derives repo-backed support-kind aggregates for dashboard analytics tie-break launches from stored selection provenance + - computes launch counts, outcome counts, success rate, and average mistakes separately for each support kind +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - adds a shared support-kind split formatter for template analytics detail + - appends the new repo-backed support-kind comparison to the live template launch detail, template launch history, and template-backed recap through the existing analytics detail fragment + +## Product effect + +Dashboard template analytics can now compare two different kinds of analytics tie-break wins for the same template: + +- broader template-scoped outcome signal tie-break launches +- selector-split-confidence-backed tie-break launches + +Those support-kind splits are now visible anywhere the dashboard already shows shared template analytics detail, and they are derived from stored run provenance rather than widget-retained state. + +## Acceptance criteria + +- repo-backed template launch scoped stats split analytics tie-break outcomes by support kind +- dashboard template analytics detail shows the new support-kind comparison when data exists +- live template launch detail, template launch history, and template-backed recap all inherit that comparison through the shared analytics detail formatter +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm repo-backed template stats derive separate support-kind buckets for analytics tie-break launches +3. confirm the dashboard analytics detail fragment shows the support-kind split comparison on live launch, history, and recap surfaces +4. confirm templates with no support-kind tie-break history show the explicit empty-state message + +That is the packet.