From bc6d234a1483ceec25f802a2f44292b8f3abd617 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Thu, 7 May 2026 00:58:00 +0200 Subject: [PATCH] Persist dashboard template selector provenance --- .../HyperTwistCoachDashboardWidget.cpp | 162 +++++++++++++++--- .../HyperTwistTrainingSubsystem.cpp | 19 ++ .../HyperTwistCoachDashboardWidget.h | 5 +- .../HyperTwistTrainingSubsystem.h | 5 + .../HyperTwistTrainingTypes.h | 39 +++++ ..._SELECTION_PROVENANCE_PACKET_2026-05-07.md | 102 +++++++++++ 6 files changed, 307 insertions(+), 25 deletions(-) create mode 100644 docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_SELECTION_PROVENANCE_PACKET_2026-05-07.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 9e14de3..e33e20d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -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 { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index d8a6fa9..0270bc1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -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, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 0ddaa8e..5648d0b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -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; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 2c75622..ad0e861 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -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, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 3699cac..330a3b4 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -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(); diff --git a/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_SELECTION_PROVENANCE_PACKET_2026-05-07.md b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_SELECTION_PROVENANCE_PACKET_2026-05-07.md new file mode 100644 index 0000000..536af16 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_SELECTION_PROVENANCE_PACKET_2026-05-07.md @@ -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.