Split template analytics tie-break outcomes by support

This commit is contained in:
axiomlogicnexus 2026-05-07 01:41:42 +02:00
parent cfd971c600
commit c65fc2a312
4 changed files with 211 additions and 28 deletions

View file

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

View file

@ -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<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;
auto FinalizeSelectionScopedAccumulator =
[](
FTemplateLaunchScopedStatsAccumulator::FSelectionScopedAccumulator& SelectionAccumulator)
{
SelectionAccumulator.Stats.OverallSuccessRate =
SelectionAccumulator.Stats.TotalCompletedAttemptCount > 0
? static_cast<float>(SelectionAccumulator.Stats.TotalSuccessCount)
/ static_cast<float>(
SelectionAccumulator.Stats.TotalCompletedAttemptCount)
: 0.0f;
SelectionAccumulator.Stats.AverageMistakesPerOutcomeRun =
SelectionAccumulator.OutcomeRunSampleCount > 0
? static_cast<float>(SelectionAccumulator.MistakeAccumulator)
/ static_cast<float>(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);

View file

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

View file

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