Persist dashboard template selector provenance

This commit is contained in:
axiomlogicnexus 2026-05-07 00:58:00 +02:00
parent a6a784862c
commit bc6d234a14
6 changed files with 307 additions and 25 deletions

View file

@ -2,6 +2,7 @@
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
@ -80,6 +81,7 @@ namespace HyperTwistCoachDashboardWidgetInternal
Entry.bPreferReviewRun = RunRecord.TemplateLaunchProvenance.bPreferReviewRun;
Entry.bCoachReview = RunRecord.TemplateLaunchProvenance.bCoachReview;
Entry.bPreferShortReviewSet = RunRecord.TemplateLaunchProvenance.bPreferShortReviewSet;
Entry.SelectionProvenance = RunRecord.TemplateLaunchProvenance.SelectionProvenance;
return Entry;
}
@ -174,20 +176,70 @@ namespace HyperTwistCoachDashboardWidgetInternal
);
}
FString BuildTemplateLaunchAnalyticsTieBreakRationale(
FHyperTwistTrainingTemplateLaunchSelectionProvenance BuildTemplateLaunchAnalyticsTieBreakSelectionProvenance(
const FHyperTwistTrainingSessionTemplate& SelectedTemplate,
const FHyperTwistTrainingTemplateLaunchScopedStats* SelectedStats,
const FHyperTwistTrainingSessionTemplate& PeerTemplate,
const FHyperTwistTrainingTemplateLaunchScopedStats* PeerStats
)
{
return FString::Printf(
FHyperTwistTrainingTemplateLaunchSelectionProvenance Provenance;
Provenance.SelectorSourceLabel = TEXT("CoachDashboard");
Provenance.ReasonClass =
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::AnalyticsTieBreak;
Provenance.PeerTemplateId = PeerTemplate.TemplateId;
Provenance.PeerTemplateTitle = ResolveDashboardTemplateLaunchDisplayTitle(PeerTemplate);
Provenance.DecisionSummaryLine = FString::Printf(
TEXT("Selector: tied with %s on primary dashboard priority; analytics preferred %s on stored outcome signal: chosen %s | peer %s."),
*ResolveDashboardTemplateLaunchDisplayTitle(PeerTemplate),
*ResolveDashboardTemplateLaunchDisplayTitle(SelectedTemplate),
*BuildTemplateLaunchAnalyticsTieBreakSignalFragment(SelectedStats),
*BuildTemplateLaunchAnalyticsTieBreakSignalFragment(PeerStats)
);
return Provenance;
}
FString ResolveTemplateLaunchSelectionReasonClassLabel(
const EHyperTwistTrainingTemplateLaunchSelectionReasonClass ReasonClass
)
{
switch (ReasonClass)
{
case EHyperTwistTrainingTemplateLaunchSelectionReasonClass::PrimaryDashboardPriority:
return TEXT("Primary Dashboard Priority");
case EHyperTwistTrainingTemplateLaunchSelectionReasonClass::AnalyticsTieBreak:
return TEXT("Analytics Tie Break");
case EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None:
default:
return TEXT("None");
}
}
FString BuildTemplateLaunchSelectionDetailFragment(
const FHyperTwistTrainingTemplateLaunchSelectionProvenance* SelectionProvenance
)
{
if (SelectionProvenance == nullptr || !SelectionProvenance->IsStructurallyValid())
{
return FString();
}
const FString PeerTemplateLabel = !SelectionProvenance->PeerTemplateTitle.IsEmpty()
? SelectionProvenance->PeerTemplateTitle
: (!SelectionProvenance->PeerTemplateId.IsEmpty()
? SelectionProvenance->PeerTemplateId
: TEXT("n/a"));
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
);
}
struct FClosureRecoveryDashboardBias
@ -3772,7 +3824,9 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiv
return FHyperTwistTrainingRunState();
}
const FHyperTwistTrainingSessionTemplate* PreferredTemplate = FindPreferredDashboardTemplateLaunch();
FHyperTwistTrainingTemplateLaunchSelectionProvenance LaunchSelectionProvenance;
const FHyperTwistTrainingSessionTemplate* PreferredTemplate =
FindPreferredDashboardTemplateLaunch(&LaunchSelectionProvenance);
if (PreferredTemplate == nullptr)
{
UpdateDashboardPresentation();
@ -3797,7 +3851,40 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiv
);
if (RunState.Session.IsStructurallyValid())
{
CaptureActiveTemplateLaunchContext(RunState.Session.TrainingSessionId, LaunchedTemplate);
if (LaunchSelectionProvenance.IsStructurallyValid())
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem =
UHyperTwistTrainingRuntimeLibrary::GetTrainingSubsystem(this))
{
if (TrainingSubsystem->RecordActiveRunTemplateLaunchSelectionProvenance(
LaunchSelectionProvenance))
{
RefreshTrainingState();
}
}
}
FHyperTwistTrainingTemplateLaunchProvenance EffectiveLaunchProvenance =
CachedRunState.Session.TrainingSessionId == RunState.Session.TrainingSessionId
&& CachedRunState.TemplateLaunchProvenance.IsStructurallyValid()
? CachedRunState.TemplateLaunchProvenance
: RunState.TemplateLaunchProvenance;
if (!EffectiveLaunchProvenance.IsStructurallyValid())
{
EffectiveLaunchProvenance =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchProvenance(LaunchedTemplate);
}
if (LaunchSelectionProvenance.IsStructurallyValid()
&& EffectiveLaunchProvenance.IsStructurallyValid()
&& !EffectiveLaunchProvenance.SelectionProvenance.IsStructurallyValid())
{
EffectiveLaunchProvenance.SelectionProvenance = LaunchSelectionProvenance;
}
CaptureActiveTemplateLaunchContext(
RunState.Session.TrainingSessionId,
EffectiveLaunchProvenance
);
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
@ -7172,12 +7259,12 @@ const FHyperTwistCoachBrief* UHyperTwistCoachDashboardWidget::GetActiveGuidanceP
}
const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindPreferredDashboardTemplateLaunch(
FString* OutSelectionRationale
FHyperTwistTrainingTemplateLaunchSelectionProvenance* OutSelectionProvenance
) const
{
if (OutSelectionRationale != nullptr)
if (OutSelectionProvenance != nullptr)
{
OutSelectionRationale->Reset();
*OutSelectionProvenance = FHyperTwistTrainingTemplateLaunchSelectionProvenance();
}
const FString EffectiveDefaultUserId = DefaultUserId;
@ -7191,9 +7278,9 @@ const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindP
{
PreferredTemplate = &SessionTemplate;
PreferredScore = TemplateScore;
if (OutSelectionRationale != nullptr)
if (OutSelectionProvenance != nullptr)
{
OutSelectionRationale->Reset();
*OutSelectionProvenance = FHyperTwistTrainingTemplateLaunchSelectionProvenance();
}
continue;
}
@ -7222,10 +7309,10 @@ const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindP
{
const FHyperTwistTrainingSessionTemplate& PreviousPreferredTemplate = *PreferredTemplate;
PreferredTemplate = &SessionTemplate;
if (OutSelectionRationale != nullptr)
if (OutSelectionProvenance != nullptr)
{
*OutSelectionRationale =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsTieBreakRationale(
*OutSelectionProvenance =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsTieBreakSelectionProvenance(
SessionTemplate,
CandidateStats,
PreviousPreferredTemplate,
@ -7288,12 +7375,11 @@ void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext(
void UHyperTwistCoachDashboardWidget::CaptureActiveTemplateLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingSessionTemplate& SessionTemplate
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance
)
{
ActiveTemplateLaunchSessionId = SessionId;
ActiveTemplateLaunchProvenance =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchProvenance(SessionTemplate);
ActiveTemplateLaunchProvenance = TemplateLaunchProvenance;
}
void UHyperTwistCoachDashboardWidget::CaptureActiveMethodDrillFollowUpLaunchContext(
@ -11505,6 +11591,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
DisplayedTemplateLaunch->TemplateId,
DisplayedTemplateLaunchUserId)
: nullptr;
const FString DisplayedTemplateLaunchSelectionDetail =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchSelectionDetailFragment(
DisplayedTemplateLaunch != nullptr
&& DisplayedTemplateLaunch->SelectionProvenance.IsStructurallyValid()
? &DisplayedTemplateLaunch->SelectionProvenance
: nullptr
);
const bool bDisplayedTemplateLaunchUsesReviewLane =
DisplayedTemplateLaunch != nullptr
&& (DisplayedTemplateLaunch->bPreferReviewRun
@ -11546,9 +11639,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
const bool bDisplayedGuidanceComparisonSingleCase = bHasActiveGuidanceComparison
? bActiveGuidanceLaunchSingleCase
: (bHasDisplayedRetainedGuidanceComparison ? bRetainedGuidanceComparisonSingleCase : false);
FString PreferredTemplateLaunchSelectionRationale;
FHyperTwistTrainingTemplateLaunchSelectionProvenance PreferredTemplateLaunchSelectionProvenance;
const FHyperTwistTrainingSessionTemplate* PreferredTemplateLaunch =
FindPreferredDashboardTemplateLaunch(&PreferredTemplateLaunchSelectionRationale);
FindPreferredDashboardTemplateLaunch(&PreferredTemplateLaunchSelectionProvenance);
int32 EligibleTemplateLaunchCount = 0;
for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : CachedActiveTrainingSessionTemplates)
{
@ -11580,6 +11673,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
PreferredTemplateLaunch->TemplateId,
PreferredTemplateLaunchUserId)
: nullptr;
const FString PreferredTemplateLaunchSelectionDetail =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchSelectionDetailFragment(
PreferredTemplateLaunchSelectionProvenance.IsStructurallyValid()
? &PreferredTemplateLaunchSelectionProvenance
: nullptr
);
const bool bCanLaunchPreferredTemplate =
!bHasLiveAttemptTimer && !bHasActiveRun && bHasPreferredTemplateLaunch;
FString TemplateLaunchStatusLine;
@ -11662,10 +11761,10 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment(
PreferredTemplateLaunchStats)
);
if (!PreferredTemplateLaunchSelectionRationale.IsEmpty())
if (!PreferredTemplateLaunchSelectionDetail.IsEmpty())
{
TemplateLaunchDetailLine += TEXT(" | ");
TemplateLaunchDetailLine += PreferredTemplateLaunchSelectionRationale;
TemplateLaunchDetailLine += PreferredTemplateLaunchSelectionDetail;
}
}
const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry* SelectedTemplateLaunchHistoryEntry =
@ -11679,6 +11778,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
SelectedTemplateLaunchHistoryEntry->TemplateId,
SelectedTemplateLaunchHistoryEntry->UserId)
: nullptr;
const FString SelectedTemplateLaunchHistorySelectionDetail =
HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchSelectionDetailFragment(
SelectedTemplateLaunchHistoryEntry != nullptr
&& SelectedTemplateLaunchHistoryEntry->SelectionProvenance.IsStructurallyValid()
? &SelectedTemplateLaunchHistoryEntry->SelectionProvenance
: nullptr
);
const FString TemplateLaunchHistoryStatusLine =
SelectedTemplateLaunchHistoryEntry != nullptr
? FString::Printf(
@ -11697,7 +11803,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
SelectedTemplateLaunchHistoryStats))
: TEXT("Template launch history: no completed template-backed runs retained yet.");
const FString TemplateLaunchHistoryDetailLine =
FString TemplateLaunchHistoryDetailLine =
SelectedTemplateLaunchHistoryEntry != nullptr
? FString::Printf(
TEXT("Title: %s | kind: %s | mode: %s | selection: %s | deck: %s | user: %s | source plan: %s | recommended %d | focus %d | attempts %d | success %.2f | mistakes %d | review-backed: %s | coach-review: %s | short set: %s | Aggregate detail: %s"),
@ -11730,6 +11836,11 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment(
SelectedTemplateLaunchHistoryStats))
: TEXT("Template launch history detail: completed runs started through the dashboard template lane will accumulate here with template id, template kind, and source review-plan provenance.");
if (!SelectedTemplateLaunchHistorySelectionDetail.IsEmpty())
{
TemplateLaunchHistoryDetailLine += TEXT(" | ");
TemplateLaunchHistoryDetailLine += SelectedTemplateLaunchHistorySelectionDetail;
}
const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun;
FString CoachHandoffStatusLine;
if (bHasLiveAttemptTimer)
@ -15740,7 +15851,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
if (bHasDisplayedRunRecap)
{
RunRecapDetailTextBlock->SetText(FText::FromString(
FString RunRecapDetailLine =
DisplayedTemplateLaunch != nullptr
? FString::Printf(
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s | Template aggregate %s | Template detail %s"),
@ -15787,8 +15898,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
DisplayedRunRecap->AverageRawSolveTimeMs,
DisplayedRunRecap->AverageInspectionTimeMs,
DisplayedRunRecap->TotalMistakes,
DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId)
));
DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId);
if (!DisplayedTemplateLaunchSelectionDetail.IsEmpty())
{
RunRecapDetailLine += TEXT(" | ");
RunRecapDetailLine += DisplayedTemplateLaunchSelectionDetail;
}
RunRecapDetailTextBlock->SetText(FText::FromString(RunRecapDetailLine));
}
else
{

View file

@ -2073,6 +2073,25 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingSubsystem::GetTrainingRepo
return TrainingRepositoryState;
}
bool UHyperTwistTrainingSubsystem::RecordActiveRunTemplateLaunchSelectionProvenance(
const FHyperTwistTrainingTemplateLaunchSelectionProvenance& SelectionProvenance
)
{
if (!SelectionProvenance.IsStructurallyValid()
|| !ActiveRunState.IsStructurallyValid()
|| !ActiveRunState.TemplateLaunchProvenance.IsStructurallyValid())
{
return false;
}
ActiveRunState.TemplateLaunchProvenance.SelectionProvenance = SelectionProvenance;
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RecordRunState(
TrainingRepositoryState,
ActiveRunState
);
return true;
}
bool UHyperTwistTrainingSubsystem::TryGetLatestGeneratedModeLaunchRequestForDeck(
const FString& UserId,
const FString& DeckId,

View file

@ -397,6 +397,7 @@ struct FHyperTwistCoachDashboardTemplateLaunchHistoryEntry
bool bPreferReviewRun = false;
bool bCoachReview = false;
bool bPreferShortReviewSet = false;
FHyperTwistTrainingTemplateLaunchSelectionProvenance SelectionProvenance;
bool IsStructurallyValid() const
{
@ -1588,7 +1589,7 @@ private:
const FString& UserId
) const;
const FHyperTwistTrainingSessionTemplate* FindPreferredDashboardTemplateLaunch(
FString* OutSelectionRationale = nullptr
FHyperTwistTrainingTemplateLaunchSelectionProvenance* OutSelectionProvenance = nullptr
) const;
FString ResolveDashboardHandoffStartActionSourceLabel(
EHyperTwistTrainingCoachHandoffKind HandoffKind) const;
@ -1683,7 +1684,7 @@ private:
);
void CaptureActiveTemplateLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingSessionTemplate& SessionTemplate
const FHyperTwistTrainingTemplateLaunchProvenance& TemplateLaunchProvenance
);
void CaptureActiveMethodDrillFollowUpLaunchContext(const FString& SessionId);
bool IsLiveAttemptInspectionPhase() const;

View file

@ -95,6 +95,11 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
FHyperTwistTrainingRepositoryState GetTrainingRepositoryState() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Templates")
bool RecordActiveRunTemplateLaunchSelectionProvenance(
const FHyperTwistTrainingTemplateLaunchSelectionProvenance& SelectionProvenance
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
bool TryGetLatestGeneratedModeLaunchRequestForDeck(
const FString& UserId,

View file

@ -1045,6 +1045,42 @@ enum class EHyperTwistTrainingSessionTemplateKind : uint8
Coach UMETA(DisplayName = "Coach")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingTemplateLaunchSelectionReasonClass : uint8
{
None UMETA(DisplayName = "None"),
PrimaryDashboardPriority UMETA(DisplayName = "Primary Dashboard Priority"),
AnalyticsTieBreak UMETA(DisplayName = "Analytics Tie Break")
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingTemplateLaunchSelectionProvenance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SelectorSourceLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingTemplateLaunchSelectionReasonClass ReasonClass =
EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PeerTemplateId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PeerTemplateTitle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DecisionSummaryLine;
bool IsStructurallyValid() const
{
return !SelectorSourceLabel.IsEmpty()
&& ReasonClass != EHyperTwistTrainingTemplateLaunchSelectionReasonClass::None;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingTemplateLaunchProvenance
{
@ -1100,6 +1136,9 @@ struct FHyperTwistTrainingTemplateLaunchProvenance
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bPreferShortReviewSet = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingTemplateLaunchSelectionProvenance SelectionProvenance;
bool IsStructurallyValid() const
{
return !TemplateId.IsEmpty();

View file

@ -0,0 +1,102 @@
# HyperTwist Phase 4 template launch selection provenance packet
Created on `2026-05-07`
Status:
- first-party HyperTwist packet
- bounded Phase `4` dashboard template selection persistence slice
## Purpose
This packet persists why the dashboard chose a template when analytics broke a tied launch decision.
The open tasks were:
- stop leaving dashboard template tie-break reasoning in widget-only state
- stamp the analytics tie-break decision onto the launched run itself
- make repo-backed template run history capable of showing not just which template launched, but which tied peer it beat and why
It is not:
- a new template analytics derivation packet
- a dashboard priority rewrite packet
- a broader run-history schema redesign packet
- a new review-plan or method-drill semantics packet
## Scope
Bounded lane:
- add a small structured template-launch selection provenance payload to persisted template launch provenance
- let the dashboard preferred-template selector emit structured analytics tie-break provenance
- add a subsystem path to stamp that provenance onto the active run and re-record the run state
- carry the persisted selector provenance through dashboard template history and run recap detail surfaces
Out of scope:
- persisting selector provenance for non-dashboard launch paths
- changing which templates are eligible for dashboard launch
- changing repository analytics aggregation rules
- adding a separate selector-audit screen
## Why this was the right next packet
The previous packet explained the analytics tie-break on the live dashboard launch surface.
That still left one obvious gap:
- operators could see why the current preferred template won before launch
- but that selector decision disappeared into widget state once the run was started
- and repo-backed run history still knew only which template launched, not which tied peer it beat or why
So the next honest move was:
- persist the selector decision into the launched run provenance itself
- keep the payload small and structured
- and let the existing recap/history surfaces read it back from stored run records
## What landed
Primary code changes:
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h`
- added `EHyperTwistTrainingTemplateLaunchSelectionReasonClass`
- added `FHyperTwistTrainingTemplateLaunchSelectionProvenance`
- nested the new selection provenance under `FHyperTwistTrainingTemplateLaunchProvenance`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h`
- added a subsystem entry point for recording active-run template launch selection provenance
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp`
- stamps selection provenance onto the active run and re-records that run into repository state
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h`
- widened template launch history entry state and selector/capture signatures to carry structured selection provenance
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp`
- emits structured analytics tie-break provenance from the preferred-template selector
- persists that provenance immediately after a successful dashboard template launch
- carries the persisted selector provenance into active template launch context, template launch history detail, and run recap detail
## Product effect
Template launch auditing is now durable:
- when the dashboard launches a template because analytics broke a tie, that decision is written into repo-backed run history
- the stored payload includes the selector source, reason class, tied peer template, and a concise decision summary
- dashboard template launch history can now show that selector provenance for completed runs
- template-backed run recap can show the same selector provenance after launch, not only before launch
## Acceptance criteria
- analytics tie-break selection provenance is persisted on launched template-backed runs
- the persisted payload includes reason class and tied peer template context
- dashboard template launch history can read selector provenance from stored run records
- template-backed run recap can read selector provenance from stored run provenance
- full product build succeeds
## Validation checklist
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
2. confirm an analytics tie-break template launch stamps selector provenance onto the active run
3. confirm repo-backed template launch history shows the stored selector provenance
4. confirm template-backed run recap shows the same stored selector provenance after launch completion
That is the packet.