Guard template tie-break confidence by selector split
This commit is contained in:
parent
38069d07ad
commit
c7027d2856
2 changed files with 201 additions and 9 deletions
|
|
@ -651,6 +651,13 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
return !Template.UserId.IsEmpty() ? Template.UserId : DefaultUserId;
|
||||
}
|
||||
|
||||
constexpr int32 TemplateLaunchAnalyticsTieBreakMinimumOutcomeRuns = 2;
|
||||
constexpr int32 TemplateLaunchSelectionTieBreakConfidenceMinimumTieBreakOutcomeRuns = 3;
|
||||
constexpr int32 TemplateLaunchSelectionTieBreakConfidenceMinimumPrimaryOutcomeRuns = 2;
|
||||
constexpr int32 TemplateLaunchSelectionTieBreakConfidenceMinimumScoreDelta = 2;
|
||||
constexpr float TemplateLaunchSelectionTieBreakConfidenceMinimumSuccessDelta = 0.05f;
|
||||
constexpr float TemplateLaunchSelectionTieBreakConfidenceMinimumMistakeImprovement = 0.5f;
|
||||
|
||||
int32 CountTemplateLaunchOutcomeRuns(const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats)
|
||||
{
|
||||
return TemplateStats != nullptr && TemplateStats->IsStructurallyValid()
|
||||
|
|
@ -658,6 +665,52 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
: 0;
|
||||
}
|
||||
|
||||
int32 CountTemplateLaunchSelectionOutcomeRuns(
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats* SelectionStats
|
||||
)
|
||||
{
|
||||
return SelectionStats != nullptr && SelectionStats->IsStructurallyValid()
|
||||
? SelectionStats->GetOutcomeRunCount()
|
||||
: 0;
|
||||
}
|
||||
|
||||
int32 BuildTemplateLaunchAnalyticsOutcomeSignalScore(
|
||||
const int32 OutcomeRuns,
|
||||
const float SuccessRate,
|
||||
const float AverageMistakesPerOutcomeRun,
|
||||
const int32 AbortedRunCount
|
||||
)
|
||||
{
|
||||
if (OutcomeRuns <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Score = 0;
|
||||
Score += FMath::Clamp(OutcomeRuns, 0, 4);
|
||||
Score += FMath::Clamp(FMath::RoundToInt(SuccessRate * 10.0f), 0, 10);
|
||||
Score -= FMath::Clamp(FMath::RoundToInt(AverageMistakesPerOutcomeRun * 2.0f), 0, 8);
|
||||
Score -= FMath::Clamp(AbortedRunCount, 0, 3);
|
||||
return Score;
|
||||
}
|
||||
|
||||
int32 BuildTemplateLaunchSelectionLaneAnalyticsScore(
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats* SelectionStats
|
||||
)
|
||||
{
|
||||
if (SelectionStats == nullptr || !SelectionStats->IsStructurallyValid())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return BuildTemplateLaunchAnalyticsOutcomeSignalScore(
|
||||
CountTemplateLaunchSelectionOutcomeRuns(SelectionStats),
|
||||
SelectionStats->OverallSuccessRate,
|
||||
SelectionStats->AverageMistakesPerOutcomeRun,
|
||||
SelectionStats->AbortedRunCount
|
||||
);
|
||||
}
|
||||
|
||||
int32 BuildTemplateLaunchAnalyticsTieBreakScore(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
|
|
@ -673,12 +726,54 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
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;
|
||||
return BuildTemplateLaunchAnalyticsOutcomeSignalScore(
|
||||
OutcomeRuns,
|
||||
TemplateStats->OverallSuccessRate,
|
||||
TemplateStats->AverageMistakesPerOutcomeRun,
|
||||
TemplateStats->AbortedRunCount
|
||||
);
|
||||
}
|
||||
|
||||
int32 BuildTemplateLaunchAnalyticsTieBreakConfidenceBonus(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
{
|
||||
if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& TieBreakStats =
|
||||
TemplateStats->DashboardAnalyticsTieBreakStats;
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& PrimaryStats =
|
||||
TemplateStats->DashboardPrimarySelectionStats;
|
||||
const int32 TieBreakOutcomeRuns = CountTemplateLaunchSelectionOutcomeRuns(&TieBreakStats);
|
||||
const int32 PrimaryOutcomeRuns = CountTemplateLaunchSelectionOutcomeRuns(&PrimaryStats);
|
||||
if (TieBreakOutcomeRuns < TemplateLaunchSelectionTieBreakConfidenceMinimumTieBreakOutcomeRuns
|
||||
|| PrimaryOutcomeRuns < TemplateLaunchSelectionTieBreakConfidenceMinimumPrimaryOutcomeRuns)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int32 TieBreakScore = BuildTemplateLaunchSelectionLaneAnalyticsScore(&TieBreakStats);
|
||||
const int32 PrimaryScore = BuildTemplateLaunchSelectionLaneAnalyticsScore(&PrimaryStats);
|
||||
const int32 ScoreDelta = TieBreakScore - PrimaryScore;
|
||||
if (ScoreDelta <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const float SuccessDelta = TieBreakStats.OverallSuccessRate - PrimaryStats.OverallSuccessRate;
|
||||
const float MistakeImprovement =
|
||||
PrimaryStats.AverageMistakesPerOutcomeRun - TieBreakStats.AverageMistakesPerOutcomeRun;
|
||||
if (ScoreDelta < TemplateLaunchSelectionTieBreakConfidenceMinimumScoreDelta
|
||||
&& SuccessDelta < TemplateLaunchSelectionTieBreakConfidenceMinimumSuccessDelta
|
||||
&& MistakeImprovement < TemplateLaunchSelectionTieBreakConfidenceMinimumMistakeImprovement)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool ShouldPreferTemplateByAnalytics(
|
||||
|
|
@ -688,13 +783,18 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
{
|
||||
const int32 CandidateOutcomeRuns = CountTemplateLaunchOutcomeRuns(CandidateStats);
|
||||
const int32 IncumbentOutcomeRuns = CountTemplateLaunchOutcomeRuns(IncumbentStats);
|
||||
if (CandidateOutcomeRuns < 2 && IncumbentOutcomeRuns < 2)
|
||||
if (CandidateOutcomeRuns < TemplateLaunchAnalyticsTieBreakMinimumOutcomeRuns
|
||||
&& IncumbentOutcomeRuns < TemplateLaunchAnalyticsTieBreakMinimumOutcomeRuns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 CandidateScore = BuildTemplateLaunchAnalyticsTieBreakScore(CandidateStats);
|
||||
const int32 IncumbentScore = BuildTemplateLaunchAnalyticsTieBreakScore(IncumbentStats);
|
||||
const int32 CandidateScore =
|
||||
BuildTemplateLaunchAnalyticsTieBreakScore(CandidateStats)
|
||||
+ BuildTemplateLaunchAnalyticsTieBreakConfidenceBonus(CandidateStats);
|
||||
const int32 IncumbentScore =
|
||||
BuildTemplateLaunchAnalyticsTieBreakScore(IncumbentStats)
|
||||
+ BuildTemplateLaunchAnalyticsTieBreakConfidenceBonus(IncumbentStats);
|
||||
if (CandidateScore != IncumbentScore)
|
||||
{
|
||||
return CandidateScore > IncumbentScore;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
# HyperTwist Phase 4 template launch tie-break confidence guard packet
|
||||
|
||||
Created on `2026-05-07`
|
||||
|
||||
Status:
|
||||
|
||||
- first-party HyperTwist packet
|
||||
- bounded Phase `4` dashboard template selector refinement slice
|
||||
|
||||
## Purpose
|
||||
|
||||
This packet stops sparse analytics tie-break history from reinforcing future dashboard template choices unless that tie-break lane has enough stored outcomes to outperform the same template's plain dashboard-priority lane.
|
||||
|
||||
The open tasks were:
|
||||
|
||||
- consume the new selector-split analytics back into launch selection rather than leaving them as display-only stats
|
||||
- keep analytics tie-break selection conservative under sparse history
|
||||
- require stored tie-break lane evidence to clear a real sample-and-outperformance guard before it can strengthen a future tie decision
|
||||
|
||||
It is not:
|
||||
|
||||
- a change to the primary review or recovery ranking rules
|
||||
- a new repository aggregation packet
|
||||
- a new selection provenance schema packet
|
||||
- a dashboard history or recap redesign packet
|
||||
|
||||
## Scope
|
||||
|
||||
Bounded lane:
|
||||
|
||||
- reuse the existing stored outcome tie-break scorer for both whole-template and selector-lane analytics
|
||||
- derive a small confidence bonus from selector-split stats only when the analytics tie-break lane has enough outcome runs
|
||||
- require that tie-break lane to beat the same template's dashboard primary lane on stronger-than-noise outcome signal before awarding that bonus
|
||||
- keep the bonus small so analytics confidence nudges tied selections instead of rewriting the launch model
|
||||
|
||||
Out of scope:
|
||||
|
||||
- changing which templates are eligible for dashboard launch
|
||||
- changing how repo-backed selector split stats are stored
|
||||
- persisting a new selector reason class
|
||||
- changing non-dashboard template launch paths
|
||||
|
||||
## Why this was the right next packet
|
||||
|
||||
The previous packet made it possible to see whether analytics tie-break launches were outperforming plain priority launches.
|
||||
|
||||
That immediately exposed the next gap:
|
||||
|
||||
- the selector could now display that split
|
||||
- but the selector itself still treated all stored outcome signal the same
|
||||
- and a handful of analytics-tie-break wins could start reinforcing future tied selections without proving that the tie-break lane really outperformed the template's ordinary dashboard-priority lane
|
||||
|
||||
So the next honest move was:
|
||||
|
||||
- keep the current overall analytics tie-break intact
|
||||
- let selector-split history contribute only as a small confidence nudge
|
||||
- and gate that nudge behind both minimum sample size and meaningful lane outperformance
|
||||
|
||||
## What landed
|
||||
|
||||
Primary code changes:
|
||||
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp`
|
||||
- factored the outcome-signal scorer so whole-template and selector-lane analytics use the same bounded score math
|
||||
- added selector-lane outcome counters and a small confidence bonus path
|
||||
- requires the `AnalyticsTieBreak` lane to have enough stored outcome runs and to beat the same template's `PrimaryDashboardPriority` lane on meaningful success/mistake/score signal before that bonus is applied
|
||||
- folds that bonus into the existing equal-priority dashboard analytics tie-break without changing the primary launch ranking rules
|
||||
|
||||
## Product effect
|
||||
|
||||
Dashboard template tie-break selection is now more resistant to self-reinforcing noise:
|
||||
|
||||
- tied templates still use the same overall analytics tie-break path as before
|
||||
- selector-split history only strengthens a template when the tie-break lane has enough actual outcome runs
|
||||
- that tie-break lane must also outperform the template's own primary dashboard lane before it earns extra confidence
|
||||
- sparse or inconclusive tie-break history no longer adds weight just because it exists
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- existing primary dashboard template ranking rules stay unchanged
|
||||
- selector-split analytics influence future tied selections only after clearing a minimum-sample guard
|
||||
- tie-break lane confidence is only awarded when it meaningfully beats the same template's primary dashboard lane
|
||||
- full product build succeeds
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
|
||||
2. confirm equal-priority template selection still uses the existing analytics tie-break path
|
||||
3. confirm selector-split history only adds confidence when the analytics tie-break lane has enough outcome runs and outperforms the primary lane
|
||||
4. confirm sparse selector-split history does not create extra preference on its own
|
||||
|
||||
That is the packet.
|
||||
Loading…
Add table
Reference in a new issue