Prefer stronger analytics on tied template launches

This commit is contained in:
axiomlogicnexus 2026-05-07 00:36:17 +02:00
parent 2d20d0ba60
commit c6215f194c
2 changed files with 208 additions and 0 deletions

View file

@ -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;
}
}
}

View file

@ -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.