Split template launch analytics by selector lane
This commit is contained in:
parent
bc6d234a14
commit
38069d07ad
4 changed files with 355 additions and 4 deletions
|
|
@ -104,6 +104,15 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
);
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchSelectionLaneAnalyticsSummary(
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& SelectionStats,
|
||||
const TCHAR* LaneLabel
|
||||
);
|
||||
|
||||
FString BuildTemplateLaunchSelectionSplitAnalyticsFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
);
|
||||
|
||||
FString BuildTemplateLaunchAnalyticsDetailFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
|
|
@ -114,7 +123,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 | 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 | last launch %s | last completed %s"),
|
||||
TemplateStats->AverageRecommendedCaseCount,
|
||||
TemplateStats->AverageFocusCaseCount,
|
||||
TemplateStats->AverageCompletedAttemptCount,
|
||||
|
|
@ -126,6 +135,7 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
TemplateStats->ReviewBackedLaunchCount,
|
||||
TemplateStats->CoachReviewLaunchCount,
|
||||
TemplateStats->ShortReviewSetLaunchCount,
|
||||
*BuildTemplateLaunchSelectionSplitAnalyticsFragment(TemplateStats),
|
||||
TemplateStats->LastLaunchAtUtc.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *TemplateStats->LastLaunchAtUtc,
|
||||
|
|
@ -199,6 +209,21 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
return Provenance;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingTemplateLaunchSelectionProvenance BuildTemplateLaunchPrimarySelectionProvenance(
|
||||
const FHyperTwistTrainingSessionTemplate& SelectedTemplate
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingTemplateLaunchSelectionProvenance Provenance;
|
||||
Provenance.SelectorSourceLabel = TEXT("CoachDashboard");
|
||||
Provenance.ReasonClass =
|
||||
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::PrimaryDashboardPriority;
|
||||
Provenance.DecisionSummaryLine = FString::Printf(
|
||||
TEXT("Selector: selected %s on dashboard priority lane without an analytics tie-break."),
|
||||
*ResolveDashboardTemplateLaunchDisplayTitle(SelectedTemplate)
|
||||
);
|
||||
return Provenance;
|
||||
}
|
||||
|
||||
FString ResolveTemplateLaunchSelectionReasonClassLabel(
|
||||
const EHyperTwistTrainingTemplateLaunchSelectionReasonClass ReasonClass
|
||||
)
|
||||
|
|
@ -215,6 +240,67 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
}
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchSelectionLaneAnalyticsSummary(
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& SelectionStats,
|
||||
const TCHAR* LaneLabel
|
||||
)
|
||||
{
|
||||
if (!SelectionStats.IsStructurallyValid())
|
||||
{
|
||||
return FString::Printf(TEXT("%s none yet"), LaneLabel);
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("%s launches %d | outcomes %d | success %.2f | avg mistakes %.2f"),
|
||||
LaneLabel,
|
||||
SelectionStats.LaunchCount,
|
||||
SelectionStats.GetOutcomeRunCount(),
|
||||
SelectionStats.OverallSuccessRate,
|
||||
SelectionStats.AverageMistakesPerOutcomeRun
|
||||
);
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchSelectionSplitAnalyticsFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
{
|
||||
if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid())
|
||||
{
|
||||
return TEXT("no dashboard selector split analytics are available for this template id yet");
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& PrimaryStats =
|
||||
TemplateStats->DashboardPrimarySelectionStats;
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionScopedStats& AnalyticsTieBreakStats =
|
||||
TemplateStats->DashboardAnalyticsTieBreakStats;
|
||||
if (!PrimaryStats.IsStructurallyValid() && !AnalyticsTieBreakStats.IsStructurallyValid())
|
||||
{
|
||||
return TEXT("no dashboard selector split analytics are available for this template id yet");
|
||||
}
|
||||
|
||||
FString Fragment = FString::Printf(
|
||||
TEXT("%s | %s"),
|
||||
*BuildTemplateLaunchSelectionLaneAnalyticsSummary(PrimaryStats, TEXT("priority")),
|
||||
*BuildTemplateLaunchSelectionLaneAnalyticsSummary(
|
||||
AnalyticsTieBreakStats,
|
||||
TEXT("tie-break"))
|
||||
);
|
||||
if (PrimaryStats.IsStructurallyValid()
|
||||
&& AnalyticsTieBreakStats.IsStructurallyValid()
|
||||
&& PrimaryStats.GetOutcomeRunCount() > 0
|
||||
&& AnalyticsTieBreakStats.GetOutcomeRunCount() > 0)
|
||||
{
|
||||
Fragment += FString::Printf(
|
||||
TEXT(" | delta success %+.2f | delta mistakes %+.2f"),
|
||||
AnalyticsTieBreakStats.OverallSuccessRate - PrimaryStats.OverallSuccessRate,
|
||||
AnalyticsTieBreakStats.AverageMistakesPerOutcomeRun
|
||||
- PrimaryStats.AverageMistakesPerOutcomeRun
|
||||
);
|
||||
}
|
||||
|
||||
return Fragment;
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchSelectionDetailFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionProvenance* SelectionProvenance
|
||||
)
|
||||
|
|
@ -228,14 +314,29 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
? SelectionProvenance->PeerTemplateTitle
|
||||
: (!SelectionProvenance->PeerTemplateId.IsEmpty()
|
||||
? SelectionProvenance->PeerTemplateId
|
||||
: TEXT("n/a"));
|
||||
: TEXT("n/a"));
|
||||
if (!SelectionProvenance->PeerTemplateTitle.IsEmpty()
|
||||
|| !SelectionProvenance->PeerTemplateId.IsEmpty())
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("Selector %s | reason %s | peer %s | %s"),
|
||||
SelectionProvenance->SelectorSourceLabel.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *SelectionProvenance->SelectorSourceLabel,
|
||||
*ResolveTemplateLaunchSelectionReasonClassLabel(SelectionProvenance->ReasonClass),
|
||||
*PeerTemplateLabel,
|
||||
SelectionProvenance->DecisionSummaryLine.IsEmpty()
|
||||
? TEXT("no retained selector decision summary")
|
||||
: *SelectionProvenance->DecisionSummaryLine
|
||||
);
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("Selector %s | reason %s | peer %s | %s"),
|
||||
TEXT("Selector %s | reason %s | %s"),
|
||||
SelectionProvenance->SelectorSourceLabel.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *SelectionProvenance->SelectorSourceLabel,
|
||||
*ResolveTemplateLaunchSelectionReasonClassLabel(SelectionProvenance->ReasonClass),
|
||||
*PeerTemplateLabel,
|
||||
SelectionProvenance->DecisionSummaryLine.IsEmpty()
|
||||
? TEXT("no retained selector decision summary")
|
||||
: *SelectionProvenance->DecisionSummaryLine
|
||||
|
|
@ -3834,6 +3935,13 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiv
|
|||
}
|
||||
|
||||
const FHyperTwistTrainingSessionTemplate LaunchedTemplate = *PreferredTemplate;
|
||||
if (!LaunchSelectionProvenance.IsStructurallyValid())
|
||||
{
|
||||
LaunchSelectionProvenance =
|
||||
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchPrimarySelectionProvenance(
|
||||
LaunchedTemplate
|
||||
);
|
||||
}
|
||||
const FString ResolvedUserId = LaunchedTemplate.UserId.IsEmpty()
|
||||
? DefaultUserId
|
||||
: LaunchedTemplate.UserId;
|
||||
|
|
|
|||
|
|
@ -9146,12 +9146,21 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser(
|
|||
|
||||
struct FTemplateLaunchScopedStatsAccumulator
|
||||
{
|
||||
struct FSelectionScopedAccumulator
|
||||
{
|
||||
FHyperTwistTrainingTemplateLaunchSelectionScopedStats Stats;
|
||||
int64 MistakeAccumulator = 0;
|
||||
int32 OutcomeRunSampleCount = 0;
|
||||
};
|
||||
|
||||
FHyperTwistTrainingTemplateLaunchScopedStats Stats;
|
||||
int64 RecommendedCaseAccumulator = 0;
|
||||
int64 FocusCaseAccumulator = 0;
|
||||
int64 CompletedAttemptAccumulator = 0;
|
||||
int64 MistakeAccumulator = 0;
|
||||
int32 OutcomeRunSampleCount = 0;
|
||||
FSelectionScopedAccumulator DashboardPrimarySelectionAccumulator;
|
||||
FSelectionScopedAccumulator DashboardAnalyticsTieBreakSelectionAccumulator;
|
||||
};
|
||||
|
||||
const FString ResolvedReferenceUtc =
|
||||
|
|
@ -9245,6 +9254,59 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser(
|
|||
Accumulator.Stats.ShortReviewSetLaunchCount += 1;
|
||||
}
|
||||
|
||||
auto AccumulateSelectionScopedStats =
|
||||
[&RunRecord](
|
||||
FTemplateLaunchScopedStatsAccumulator::FSelectionScopedAccumulator& SelectionAccumulator,
|
||||
const FHyperTwistTrainingTemplateLaunchSelectionProvenance& SelectionProvenance)
|
||||
{
|
||||
SelectionAccumulator.Stats.SelectorSourceLabel = SelectionProvenance.SelectorSourceLabel;
|
||||
SelectionAccumulator.Stats.ReasonClass = SelectionProvenance.ReasonClass;
|
||||
SelectionAccumulator.Stats.LaunchCount += 1;
|
||||
SelectionAccumulator.Stats.TotalCompletedAttemptCount +=
|
||||
RunRecord.Summary.CompletedAttemptCount;
|
||||
SelectionAccumulator.Stats.TotalSuccessCount += RunRecord.Summary.SuccessCount;
|
||||
|
||||
if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Completed)
|
||||
{
|
||||
SelectionAccumulator.Stats.CompletedRunCount += 1;
|
||||
}
|
||||
else if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
SelectionAccumulator.Stats.AbortedRunCount += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionAccumulator.Stats.OpenRunCount += 1;
|
||||
}
|
||||
|
||||
if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Completed
|
||||
|| RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
SelectionAccumulator.MistakeAccumulator += RunRecord.Summary.TotalMistakes;
|
||||
SelectionAccumulator.OutcomeRunSampleCount += 1;
|
||||
}
|
||||
};
|
||||
if (Provenance.SelectionProvenance.IsStructurallyValid()
|
||||
&& Provenance.SelectionProvenance.SelectorSourceLabel == TEXT("CoachDashboard"))
|
||||
{
|
||||
if (Provenance.SelectionProvenance.ReasonClass ==
|
||||
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::PrimaryDashboardPriority)
|
||||
{
|
||||
AccumulateSelectionScopedStats(
|
||||
Accumulator.DashboardPrimarySelectionAccumulator,
|
||||
Provenance.SelectionProvenance
|
||||
);
|
||||
}
|
||||
else if (Provenance.SelectionProvenance.ReasonClass ==
|
||||
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::AnalyticsTieBreak)
|
||||
{
|
||||
AccumulateSelectionScopedStats(
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator,
|
||||
Provenance.SelectionProvenance
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Completed)
|
||||
{
|
||||
Accumulator.Stats.CompletedRunCount += 1;
|
||||
|
|
@ -9301,6 +9363,37 @@ UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser(
|
|||
? static_cast<float>(Accumulator.MistakeAccumulator)
|
||||
/ static_cast<float>(Accumulator.OutcomeRunSampleCount)
|
||||
: 0.0f;
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats.OverallSuccessRate =
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalCompletedAttemptCount > 0
|
||||
? static_cast<float>(
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalSuccessCount)
|
||||
/ static_cast<float>(
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats.TotalCompletedAttemptCount)
|
||||
: 0.0f;
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats.AverageMistakesPerOutcomeRun =
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.OutcomeRunSampleCount > 0
|
||||
? static_cast<float>(Accumulator.DashboardPrimarySelectionAccumulator.MistakeAccumulator)
|
||||
/ static_cast<float>(
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.OutcomeRunSampleCount)
|
||||
: 0.0f;
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.OverallSuccessRate =
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalCompletedAttemptCount > 0
|
||||
? static_cast<float>(
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalSuccessCount)
|
||||
/ static_cast<float>(
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.TotalCompletedAttemptCount)
|
||||
: 0.0f;
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats.AverageMistakesPerOutcomeRun =
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.OutcomeRunSampleCount > 0
|
||||
? static_cast<float>(
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.MistakeAccumulator)
|
||||
/ static_cast<float>(
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.OutcomeRunSampleCount)
|
||||
: 0.0f;
|
||||
Accumulator.Stats.DashboardPrimarySelectionStats =
|
||||
Accumulator.DashboardPrimarySelectionAccumulator.Stats;
|
||||
Accumulator.Stats.DashboardAnalyticsTieBreakStats =
|
||||
Accumulator.DashboardAnalyticsTieBreakSelectionAccumulator.Stats;
|
||||
if (Accumulator.Stats.IsStructurallyValid())
|
||||
{
|
||||
ScopedStats.Add(Accumulator.Stats);
|
||||
|
|
|
|||
|
|
@ -2013,6 +2013,54 @@ struct FHyperTwistTrainingDeckScopedStats
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingTemplateLaunchSelectionScopedStats
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SelectorSourceLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingTemplateLaunchSelectionReasonClass ReasonClass =
|
||||
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LaunchCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 CompletedRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AbortedRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 OpenRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalCompletedAttemptCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalSuccessCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
float OverallSuccessRate = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
float AverageMistakesPerOutcomeRun = 0.0f;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return ReasonClass != EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None
|
||||
&& LaunchCount > 0;
|
||||
}
|
||||
|
||||
int32 GetOutcomeRunCount() const
|
||||
{
|
||||
return CompletedRunCount + AbortedRunCount;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingTemplateLaunchScopedStats
|
||||
{
|
||||
|
|
@ -2107,6 +2155,12 @@ struct FHyperTwistTrainingTemplateLaunchScopedStats
|
|||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
float AverageMistakesPerOutcomeRun = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingTemplateLaunchSelectionScopedStats DashboardPrimarySelectionStats;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FHyperTwistTrainingTemplateLaunchSelectionScopedStats DashboardAnalyticsTieBreakStats;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastLaunchAtUtc;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
# HyperTwist Phase 4 template launch selection split analytics packet
|
||||
|
||||
Created on `2026-05-07`
|
||||
|
||||
Status:
|
||||
|
||||
- first-party HyperTwist packet
|
||||
- bounded Phase `4` dashboard template analytics follow-through slice
|
||||
|
||||
## Purpose
|
||||
|
||||
This packet splits repo-backed template launch outcomes by dashboard selector reason class so operators can compare plain priority picks against analytics tie-break picks.
|
||||
|
||||
The open tasks were:
|
||||
|
||||
- stop treating persisted selector reason class as audit-only metadata
|
||||
- derive per-template outcome aggregates separately for dashboard primary-priority launches and analytics tie-break launches
|
||||
- surface that stored split back into the dashboard template launch detail, history, and recap lanes
|
||||
|
||||
It is not:
|
||||
|
||||
- a new dashboard selector rewrite packet
|
||||
- a non-dashboard analytics packet
|
||||
- a new repository history schema packet beyond template-scoped aggregates
|
||||
- a broader coach weighting redesign packet
|
||||
|
||||
## Scope
|
||||
|
||||
Bounded lane:
|
||||
|
||||
- add a small selection-scoped aggregate struct alongside template launch scoped stats
|
||||
- derive dashboard `PrimaryDashboardPriority` and `AnalyticsTieBreak` aggregates from persisted run provenance
|
||||
- stamp primary dashboard launches with explicit selector provenance so both selector lanes are counted
|
||||
- append selector split analytics to the existing per-template analytics detail fragment already used by dashboard launch, history, and recap surfaces
|
||||
|
||||
Out of scope:
|
||||
|
||||
- changing the primary launch priority rules
|
||||
- changing analytics tie-break formulas
|
||||
- adding a new dedicated selector analytics screen
|
||||
- aggregating selector lanes for non-dashboard launch sources
|
||||
|
||||
## Why this was the right next packet
|
||||
|
||||
The previous packet persisted why the dashboard picked a template.
|
||||
|
||||
That closed the audit gap for individual runs, but it still left a product-level question unanswered:
|
||||
|
||||
- did analytics tie-break picks actually outperform plain dashboard priority picks for the same template lane
|
||||
- or were they only explainable, not measurable
|
||||
|
||||
So the next honest move was:
|
||||
|
||||
- treat selector reason class as an analytics dimension, not only a history detail
|
||||
- derive that split directly from repo-backed run records
|
||||
- and surface the comparison where operators already inspect template launch outcomes
|
||||
|
||||
## What landed
|
||||
|
||||
Primary code changes:
|
||||
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h`
|
||||
- added `FHyperTwistTrainingTemplateLaunchSelectionScopedStats`
|
||||
- added per-template `DashboardPrimarySelectionStats` and `DashboardAnalyticsTieBreakStats`
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp`
|
||||
- derives selector-scoped launch, completion, abort, success, and average-mistake aggregates from persisted template launch provenance
|
||||
- splits those aggregates specifically for dashboard primary-priority vs analytics tie-break launches
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp`
|
||||
- stamps primary dashboard template launches with explicit selector provenance
|
||||
- adds selector split formatting helpers
|
||||
- appends stored selector split analytics to the existing template analytics detail fragment used by the live launch row, template launch history, and run recap
|
||||
|
||||
## Product effect
|
||||
|
||||
Template launch analytics are now split by how the dashboard chose the template:
|
||||
|
||||
- plain dashboard priority launches and analytics tie-break launches accumulate separately per template id
|
||||
- the dashboard can compare those two lanes using repo-backed success and mistake outcomes
|
||||
- the live template launch section, template launch history, and template-backed run recap all inherit that selector split automatically through the shared analytics detail fragment
|
||||
- templates with no stored selector split history remain explicit about that gap instead of implying a comparison exists
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- repo-backed template launch analytics split dashboard primary-priority vs analytics tie-break launches
|
||||
- primary dashboard launches record selector provenance so the primary lane is measurable
|
||||
- dashboard template analytics detail surfaces show the selector split comparison when data exists
|
||||
- full product build succeeds
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
|
||||
2. confirm template-scoped stats derive separate aggregates for dashboard primary and analytics tie-break selection reasons
|
||||
3. confirm live template launch detail, template launch history, and template-backed recap all show the selector split analytics fragment
|
||||
4. confirm templates without selector split history show the explicit empty-state message
|
||||
|
||||
That is the packet.
|
||||
Loading…
Add table
Reference in a new issue