From c17bfca008781f5947fb89e2a30049cd601b57d2 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 6 May 2026 23:40:31 +0200 Subject: [PATCH] Persist template launch provenance in run history --- .../HyperTwistCoachDashboardWidget.cpp | 234 +++++++++++++----- .../HyperTwistTrainingRepositoryLibrary.cpp | 5 +- .../HyperTwistTrainingSubsystem.cpp | 106 +++++++- .../HyperTwistCoachDashboardWidget.h | 6 +- .../HyperTwistTrainingTypes.h | 87 ++++++- ...REPOSITORY_PROVENANCE_PACKET_2026-05-06.md | 105 ++++++++ 6 files changed, 467 insertions(+), 76 deletions(-) create mode 100644 docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_REPOSITORY_PROVENANCE_PACKET_2026-05-06.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 8e9a8f4..c4c0b5c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -1,5 +1,7 @@ #include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h" +#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h" #include "Blueprint/WidgetTree.h" #include "Components/Button.h" #include "Components/ButtonSlot.h" @@ -13,6 +15,74 @@ namespace HyperTwistCoachDashboardWidgetInternal { + FHyperTwistTrainingTemplateLaunchProvenance BuildTemplateLaunchProvenance( + const FHyperTwistTrainingSessionTemplate& SessionTemplate + ) + { + FHyperTwistTrainingTemplateLaunchProvenance Provenance; + if (!SessionTemplate.IsStructurallyValid()) + { + return Provenance; + } + + Provenance.UserId = SessionTemplate.UserId; + Provenance.TemplateId = SessionTemplate.TemplateId; + Provenance.SourceTemplateId = SessionTemplate.SourceTemplateId; + Provenance.Title = SessionTemplate.Title; + Provenance.TemplateKind = SessionTemplate.TemplateKind; + Provenance.FocusDeckId = SessionTemplate.FocusDeckId; + Provenance.MethodSegmentId = SessionTemplate.MethodSegmentId; + Provenance.SuggestedMode = SessionTemplate.SuggestedMode; + Provenance.SuggestedSelectionPolicy = SessionTemplate.SuggestedSelectionPolicy; + Provenance.RecommendedCaseCount = SessionTemplate.RecommendedCaseCount; + Provenance.FocusCaseIds = SessionTemplate.FocusCaseIds; + Provenance.GuidanceLabel = SessionTemplate.GuidanceLabel; + Provenance.SourceReviewPlanId = SessionTemplate.SourceReviewPlanId; + Provenance.bPreferReviewRun = SessionTemplate.bPreferReviewRun; + Provenance.bCoachReview = SessionTemplate.bCoachReview; + Provenance.bPreferShortReviewSet = SessionTemplate.bPreferShortReviewSet; + return Provenance; + } + + FHyperTwistCoachDashboardTemplateLaunchHistoryEntry BuildTemplateLaunchHistoryEntry( + const FHyperTwistTrainingRunRecord& RunRecord + ) + { + FHyperTwistCoachDashboardTemplateLaunchHistoryEntry Entry; + if (!RunRecord.IsStructurallyValid() || !RunRecord.TemplateLaunchProvenance.IsStructurallyValid()) + { + return Entry; + } + + Entry.RecordedAtUtc = !RunRecord.LastUpdatedAtUtc.IsEmpty() + ? RunRecord.LastUpdatedAtUtc + : (!RunRecord.Summary.LastCompletedAtUtc.IsEmpty() + ? RunRecord.Summary.LastCompletedAtUtc + : RunRecord.Session.EndedAtUtc); + Entry.SourceSessionId = RunRecord.Session.TrainingSessionId; + Entry.TemplateId = RunRecord.TemplateLaunchProvenance.TemplateId; + Entry.TemplateTitle = RunRecord.TemplateLaunchProvenance.Title; + Entry.UserId = RunRecord.TemplateLaunchProvenance.UserId; + Entry.FocusDeckId = RunRecord.TemplateLaunchProvenance.FocusDeckId; + Entry.SourceReviewPlanId = RunRecord.TemplateLaunchProvenance.SourceReviewPlanId; + Entry.TemplateKind = RunRecord.TemplateLaunchProvenance.TemplateKind; + Entry.SessionState = RunRecord.Summary.SessionState; + Entry.SuggestedMode = RunRecord.TemplateLaunchProvenance.SuggestedMode; + Entry.SuggestedSelectionPolicy = RunRecord.TemplateLaunchProvenance.SuggestedSelectionPolicy; + Entry.RecommendedCaseCount = RunRecord.TemplateLaunchProvenance.RecommendedCaseCount; + Entry.FocusCaseCount = RunRecord.TemplateLaunchProvenance.FocusCaseIds.Num(); + Entry.CompletedAttemptCount = RunRecord.Summary.CompletedAttemptCount; + Entry.TotalMistakes = RunRecord.Summary.TotalMistakes; + Entry.SuccessRate = RunRecord.Summary.SuccessRate; + Entry.CompletedAtUtc = !RunRecord.Summary.LastCompletedAtUtc.IsEmpty() + ? RunRecord.Summary.LastCompletedAtUtc + : RunRecord.Session.EndedAtUtc; + Entry.bPreferReviewRun = RunRecord.TemplateLaunchProvenance.bPreferReviewRun; + Entry.bCoachReview = RunRecord.TemplateLaunchProvenance.bCoachReview; + Entry.bPreferShortReviewSet = RunRecord.TemplateLaunchProvenance.bPreferShortReviewSet; + return Entry; + } + struct FClosureRecoveryDashboardBias { int32 ContinuationBias = 0; @@ -3697,7 +3767,7 @@ void UHyperTwistCoachDashboardWidget::ClearRetainedRunRecap() RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; bHasRetainedTemplateLaunch = false; RetainedTemplateLaunchSessionId.Reset(); - RetainedTemplateLaunchTemplate = FHyperTwistTrainingSessionTemplate(); + RetainedTemplateLaunchProvenance = FHyperTwistTrainingTemplateLaunchProvenance(); bHasRetainedGuidanceComparison = false; RetainedGuidanceComparisonSessionId.Reset(); RetainedGuidanceComparisonLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; @@ -6653,6 +6723,13 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedAttempt() void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() { + if (CachedRunState.Session.IsStructurallyValid() + && CachedRunState.TemplateLaunchProvenance.IsStructurallyValid()) + { + ActiveTemplateLaunchSessionId = CachedRunState.Session.TrainingSessionId; + ActiveTemplateLaunchProvenance = CachedRunState.TemplateLaunchProvenance; + } + if (CachedRunState.Session.IsStructurallyValid() && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)) @@ -6665,12 +6742,11 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() RetainedRunRecapMode = CachedRunState.Session.Mode; if (!ActiveTemplateLaunchSessionId.IsEmpty() && ActiveTemplateLaunchSessionId == RetainedRunRecapSessionId - && ActiveTemplateLaunchTemplate.IsStructurallyValid()) + && ActiveTemplateLaunchProvenance.IsStructurallyValid()) { bHasRetainedTemplateLaunch = true; RetainedTemplateLaunchSessionId = ActiveTemplateLaunchSessionId; - RetainedTemplateLaunchTemplate = ActiveTemplateLaunchTemplate; - RecordTemplateLaunchHistoryEntry(RetainedRunRecapSummary); + RetainedTemplateLaunchProvenance = ActiveTemplateLaunchProvenance; } if (!ActiveGuidanceLaunchSessionId.IsEmpty() && ActiveGuidanceLaunchSessionId == RetainedRunRecapSessionId @@ -6698,6 +6774,8 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() ActiveMethodDrillFollowUpLaunchPacketStanceLabel.Reset(); } } + + SyncTemplateLaunchHistoryFromRepository(); } void UHyperTwistCoachDashboardWidget::SyncSelectedGuidancePreviewCase() @@ -6939,7 +7017,8 @@ void UHyperTwistCoachDashboardWidget::CaptureActiveTemplateLaunchContext( ) { ActiveTemplateLaunchSessionId = SessionId; - ActiveTemplateLaunchTemplate = SessionTemplate; + ActiveTemplateLaunchProvenance = + HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchProvenance(SessionTemplate); } void UHyperTwistCoachDashboardWidget::CaptureActiveMethodDrillFollowUpLaunchContext( @@ -9858,63 +9937,100 @@ void UHyperTwistCoachDashboardWidget::RecordClosureRecoverySnapshot() SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1; } -void UHyperTwistCoachDashboardWidget::RecordTemplateLaunchHistoryEntry( - const FHyperTwistTrainingSessionSummary& CompletedRunRecap -) +void UHyperTwistCoachDashboardWidget::SyncTemplateLaunchHistoryFromRepository() { - if (CompletedRunRecap.TrainingSessionId.IsEmpty() - || !ActiveTemplateLaunchTemplate.IsStructurallyValid() - || ActiveTemplateLaunchSessionId != CompletedRunRecap.TrainingSessionId) + const FString PreviouslySelectedSessionId = + SelectedTemplateLaunchHistoryIndex >= 0 + && SelectedTemplateLaunchHistoryIndex < TemplateLaunchHistoryEntries.Num() + ? TemplateLaunchHistoryEntries[SelectedTemplateLaunchHistoryIndex].SourceSessionId + : FString(); + + TemplateLaunchHistoryEntries.Reset(); + SelectedTemplateLaunchHistoryIndex = INDEX_NONE; + + FString HistoryUserId = !DefaultUserId.IsEmpty() + ? DefaultUserId + : RetainedRunRecapUserId; + if (HistoryUserId.IsEmpty() && CachedRunState.Session.IsStructurallyValid()) + { + HistoryUserId = CachedRunState.Session.UserId; + } + if (HistoryUserId.IsEmpty() && ActiveTemplateLaunchProvenance.IsStructurallyValid()) + { + HistoryUserId = ActiveTemplateLaunchProvenance.UserId; + } + if (HistoryUserId.IsEmpty() && RetainedTemplateLaunchProvenance.IsStructurallyValid()) + { + HistoryUserId = RetainedTemplateLaunchProvenance.UserId; + } + if (HistoryUserId.IsEmpty() && CachedDefaultUserTrainingSessionTemplates.Num() > 0) + { + HistoryUserId = CachedDefaultUserTrainingSessionTemplates[0].UserId; + } + if (HistoryUserId.IsEmpty() && CachedActiveTrainingSessionTemplates.Num() > 0) + { + HistoryUserId = CachedActiveTrainingSessionTemplates[0].UserId; + } + if (HistoryUserId.IsEmpty()) { return; } - FHyperTwistCoachDashboardTemplateLaunchHistoryEntry Entry; - Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); - Entry.SourceSessionId = CompletedRunRecap.TrainingSessionId; - Entry.TemplateId = ActiveTemplateLaunchTemplate.TemplateId; - Entry.TemplateTitle = ActiveTemplateLaunchTemplate.Title; - Entry.UserId = ActiveTemplateLaunchTemplate.UserId; - Entry.FocusDeckId = ActiveTemplateLaunchTemplate.FocusDeckId; - Entry.SourceReviewPlanId = ActiveTemplateLaunchTemplate.SourceReviewPlanId; - Entry.TemplateKind = ActiveTemplateLaunchTemplate.TemplateKind; - Entry.SessionState = CompletedRunRecap.SessionState; - Entry.SuggestedMode = ActiveTemplateLaunchTemplate.SuggestedMode; - Entry.SuggestedSelectionPolicy = ActiveTemplateLaunchTemplate.SuggestedSelectionPolicy; - Entry.RecommendedCaseCount = ActiveTemplateLaunchTemplate.RecommendedCaseCount; - Entry.FocusCaseCount = ActiveTemplateLaunchTemplate.FocusCaseIds.Num(); - Entry.CompletedAttemptCount = CompletedRunRecap.CompletedAttemptCount; - Entry.TotalMistakes = CompletedRunRecap.TotalMistakes; - Entry.SuccessRate = CompletedRunRecap.SuccessRate; - Entry.CompletedAtUtc = CompletedRunRecap.LastCompletedAtUtc; - Entry.bPreferReviewRun = ActiveTemplateLaunchTemplate.bPreferReviewRun; - Entry.bCoachReview = ActiveTemplateLaunchTemplate.bCoachReview; - Entry.bPreferShortReviewSet = ActiveTemplateLaunchTemplate.bPreferShortReviewSet; - if (!Entry.IsStructurallyValid()) - { - return; - } - - const bool bAlreadyRecorded = TemplateLaunchHistoryEntries.ContainsByPredicate( - [&Entry](const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry& Candidate) - { - return Candidate.SourceSessionId == Entry.SourceSessionId; - } - ); - if (bAlreadyRecorded) - { - return; - } - - TemplateLaunchHistoryEntries.Add(Entry); + const FHyperTwistTrainingRepositoryState RepositoryState = + UHyperTwistTrainingRuntimeLibrary::GetTrainingRepositoryState(this); + const TArray UserRunRecords = + UHyperTwistTrainingRepositoryLibrary::ListRunRecordsForDeck( + RepositoryState, + HistoryUserId, + FString()); + TArray ReverseOrderedEntries; constexpr int32 MaxRetainedTemplateLaunchHistoryEntries = 16; - if (TemplateLaunchHistoryEntries.Num() > MaxRetainedTemplateLaunchHistoryEntries) + ReverseOrderedEntries.Reserve(MaxRetainedTemplateLaunchHistoryEntries); + for (const FHyperTwistTrainingRunRecord& RunRecord : UserRunRecords) { - const int32 ExcessCount = - TemplateLaunchHistoryEntries.Num() - MaxRetainedTemplateLaunchHistoryEntries; - TemplateLaunchHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); + if (!RunRecord.IsStructurallyValid() + || !RunRecord.TemplateLaunchProvenance.IsStructurallyValid() + || (RunRecord.Summary.SessionState != EHyperTwistTrainingSessionState::Completed + && RunRecord.Summary.SessionState != EHyperTwistTrainingSessionState::Aborted)) + { + continue; + } + + FHyperTwistCoachDashboardTemplateLaunchHistoryEntry Entry = + HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchHistoryEntry(RunRecord); + if (!Entry.IsStructurallyValid()) + { + continue; + } + + ReverseOrderedEntries.Add(MoveTemp(Entry)); + if (ReverseOrderedEntries.Num() >= MaxRetainedTemplateLaunchHistoryEntries) + { + break; + } } - SelectedTemplateLaunchHistoryIndex = TemplateLaunchHistoryEntries.Num() - 1; + + for (int32 Index = ReverseOrderedEntries.Num() - 1; Index >= 0; --Index) + { + TemplateLaunchHistoryEntries.Add(ReverseOrderedEntries[Index]); + } + + if (!PreviouslySelectedSessionId.IsEmpty()) + { + const int32 MatchingIndex = TemplateLaunchHistoryEntries.IndexOfByPredicate( + [&PreviouslySelectedSessionId]( + const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry& Candidate) + { + return Candidate.SourceSessionId == PreviouslySelectedSessionId; + } + ); + if (MatchingIndex >= 0) + { + SelectedTemplateLaunchHistoryIndex = MatchingIndex; + } + } + + SyncSelectedTemplateLaunchHistoryEntry(); } void UHyperTwistCoachDashboardWidget::RecordMethodDrillFollowUpHistoryEntry( @@ -11083,18 +11199,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() && DisplayedRunRecap != nullptr && !ActiveTemplateLaunchSessionId.IsEmpty() && DisplayedRunRecap->TrainingSessionId == ActiveTemplateLaunchSessionId - && ActiveTemplateLaunchTemplate.IsStructurallyValid(); + && ActiveTemplateLaunchProvenance.IsStructurallyValid(); const bool bHasDisplayedRetainedTemplateLaunch = !bHasCurrentRunRecap && bHasRetainedTemplateLaunch && DisplayedRunRecap != nullptr && !RetainedTemplateLaunchSessionId.IsEmpty() && DisplayedRunRecap->TrainingSessionId == RetainedTemplateLaunchSessionId - && RetainedTemplateLaunchTemplate.IsStructurallyValid(); - const FHyperTwistTrainingSessionTemplate* DisplayedTemplateLaunch = + && RetainedTemplateLaunchProvenance.IsStructurallyValid(); + const FHyperTwistTrainingTemplateLaunchProvenance* DisplayedTemplateLaunch = bHasActiveTemplateLaunchForDisplayedRun - ? &ActiveTemplateLaunchTemplate - : (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchTemplate : nullptr); + ? &ActiveTemplateLaunchProvenance + : (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchProvenance : nullptr); const bool bDisplayedTemplateLaunchUsesReviewLane = DisplayedTemplateLaunch != nullptr && (DisplayedTemplateLaunch->bPreferReviewRun diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index db09e4b..6275961 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -6212,8 +6212,9 @@ FHyperTwistTrainingRunRecord UHyperTwistTrainingRepositoryLibrary::BuildRunRecor RunState.Attempts, RunState.ImportedRuntimeSelectionState ); - RunRecord.ImportedRuntimeSelectionState = RunState.ImportedRuntimeSelectionState; - RunRecord.ImportedGeneratedModeLaunchRequest = RunState.ImportedGeneratedModeLaunchRequest; + RunRecord.ImportedRuntimeSelectionState = RunState.ImportedRuntimeSelectionState; + RunRecord.ImportedGeneratedModeLaunchRequest = RunState.ImportedGeneratedModeLaunchRequest; + RunRecord.TemplateLaunchProvenance = RunState.TemplateLaunchProvenance; RunRecord.Attempts = RunState.Attempts; RunRecord.RemainingCaseIds = RunState.RemainingCaseIds; RunRecord.ReplayId = RunState.ReplayPacket.ReplayId; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 524621d..d8a6fa9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -188,6 +188,60 @@ namespace HyperTwistTrainingSubsystemInternal return FString(); } + FHyperTwistTrainingTemplateLaunchProvenance BuildTemplateLaunchProvenance( + const FHyperTwistTrainingSessionTemplate& SessionTemplate + ) + { + FHyperTwistTrainingTemplateLaunchProvenance Provenance; + if (!SessionTemplate.IsStructurallyValid()) + { + return Provenance; + } + + Provenance.UserId = SessionTemplate.UserId; + Provenance.TemplateId = SessionTemplate.TemplateId; + Provenance.SourceTemplateId = SessionTemplate.SourceTemplateId; + Provenance.Title = SessionTemplate.Title; + Provenance.TemplateKind = SessionTemplate.TemplateKind; + Provenance.FocusDeckId = SessionTemplate.FocusDeckId; + Provenance.MethodSegmentId = SessionTemplate.MethodSegmentId; + Provenance.SuggestedMode = SessionTemplate.SuggestedMode; + Provenance.SuggestedSelectionPolicy = SessionTemplate.SuggestedSelectionPolicy; + Provenance.RecommendedCaseCount = SessionTemplate.RecommendedCaseCount; + Provenance.FocusCaseIds = SessionTemplate.FocusCaseIds; + Provenance.GuidanceLabel = SessionTemplate.GuidanceLabel; + Provenance.SourceReviewPlanId = SessionTemplate.SourceReviewPlanId; + Provenance.bPreferReviewRun = SessionTemplate.bPreferReviewRun; + Provenance.bCoachReview = SessionTemplate.bCoachReview; + Provenance.bPreferShortReviewSet = SessionTemplate.bPreferShortReviewSet; + return Provenance; + } + + void PersistActiveRunTemplateLaunchProvenance( + FHyperTwistTrainingRepositoryState& RepositoryState, + FHyperTwistTrainingRunState& ActiveRunState, + const FHyperTwistTrainingSessionTemplate& SessionTemplate + ) + { + if (!ActiveRunState.IsStructurallyValid()) + { + return; + } + + const FHyperTwistTrainingTemplateLaunchProvenance Provenance = + BuildTemplateLaunchProvenance(SessionTemplate); + if (!Provenance.IsStructurallyValid()) + { + return; + } + + ActiveRunState.TemplateLaunchProvenance = Provenance; + RepositoryState = UHyperTwistTrainingRepositoryLibrary::RecordRunState( + RepositoryState, + ActiveRunState + ); + } + FString FormatLaunchBudgetLabel(const int32 CaseCount) { return FString::Printf(TEXT("Launch cases: %d"), CaseCount); @@ -3616,11 +3670,22 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromTe ActiveReviewPlanState, ResolvedUserId)) { - return StartRecommendedReviewRun( + FHyperTwistTrainingRunState RunState = StartRecommendedReviewRun( SessionId, MaterializedDeck.Cases.Num(), MatchingTemplate->SuggestedMode ); + HyperTwistTrainingSubsystemInternal::PersistActiveRunTemplateLaunchProvenance( + TrainingRepositoryState, + ActiveRunState, + *MatchingTemplate + ); + if (ActiveRunState.IsStructurallyValid()) + { + RunState = ActiveRunState; + RefreshRepositoryViews(); + } + return RunState; } FHyperTwistTrainingDeck SourceDeck; @@ -3643,10 +3708,23 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromTe SessionId, MatchingTemplate->SuggestedMode ); + HyperTwistTrainingSubsystemInternal::PersistActiveRunTemplateLaunchProvenance( + TrainingRepositoryState, + ActiveRunState, + *MatchingTemplate + ); + if (ActiveRunState.IsStructurallyValid()) + { + RunState = ActiveRunState; + } if (!RunState.IsStructurallyValid() || !MatchingTemplate->bPreferReviewRun || !bPreparedReviewTemplateLaunch) { + if (RunState.IsStructurallyValid()) + { + RefreshRepositoryViews(); + } return RunState; } @@ -3713,11 +3791,22 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartActiveMethodDrill ActiveReviewPlanState, ActiveMethodDrillRunState.UserId)) { - return StartRecommendedReviewRun( + FHyperTwistTrainingRunState RunState = StartRecommendedReviewRun( StructuredSessionId, FollowUpDeck.Cases.Num(), ActiveMethodDrillFollowUpTemplate.SuggestedMode ); + HyperTwistTrainingSubsystemInternal::PersistActiveRunTemplateLaunchProvenance( + TrainingRepositoryState, + ActiveRunState, + ActiveMethodDrillFollowUpTemplate + ); + if (ActiveRunState.IsStructurallyValid()) + { + RunState = ActiveRunState; + RefreshRepositoryViews(); + } + return RunState; } FHyperTwistTrainingDeck SourceDeck; @@ -3739,10 +3828,23 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartActiveMethodDrill StructuredSessionId, ActiveMethodDrillFollowUpTemplate.SuggestedMode ); + HyperTwistTrainingSubsystemInternal::PersistActiveRunTemplateLaunchProvenance( + TrainingRepositoryState, + ActiveRunState, + ActiveMethodDrillFollowUpTemplate + ); + if (ActiveRunState.IsStructurallyValid()) + { + RunState = ActiveRunState; + } if (!RunState.IsStructurallyValid() || !ActiveMethodDrillFollowUpTemplate.bPreferReviewRun || !bPreparedReviewTemplateLaunch) { + if (RunState.IsStructurallyValid()) + { + RefreshRepositoryViews(); + } return RunState; } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index b400af8..56e8bff 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -1649,7 +1649,7 @@ private: ); void RecordQueueSuppressionSnapshot(); void RecordClosureRecoverySnapshot(); - void RecordTemplateLaunchHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); + void SyncTemplateLaunchHistoryFromRepository(); void RecordMethodDrillFollowUpHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); void RecordMethodDrillRecoveryMemoryHistorySnapshot( const FHyperTwistCoachDashboardQueueRecoveryWeighting& QueueRecoveryWeighting, @@ -1696,10 +1696,10 @@ private: FString RetainedRunRecapSessionId; EHyperTwistTrainingDeliveryMode RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; FString ActiveTemplateLaunchSessionId; - FHyperTwistTrainingSessionTemplate ActiveTemplateLaunchTemplate; + FHyperTwistTrainingTemplateLaunchProvenance ActiveTemplateLaunchProvenance; bool bHasRetainedTemplateLaunch = false; FString RetainedTemplateLaunchSessionId; - FHyperTwistTrainingSessionTemplate RetainedTemplateLaunchTemplate; + FHyperTwistTrainingTemplateLaunchProvenance RetainedTemplateLaunchProvenance; FString ActiveGuidanceLaunchSessionId; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidanceLaunchLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index b0d660b..c55553e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -1035,6 +1035,77 @@ struct FHyperTwistTrainingAttemptResolution } }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingSessionTemplateKind : uint8 +{ + ReviewPlan UMETA(DisplayName = "Review Plan"), + Recovery UMETA(DisplayName = "Recovery"), + Accuracy UMETA(DisplayName = "Accuracy"), + Speed UMETA(DisplayName = "Speed"), + Coach UMETA(DisplayName = "Coach") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingTemplateLaunchProvenance +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceTemplateId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Title; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSessionTemplateKind TemplateKind = + EHyperTwistTrainingSessionTemplateKind::Recovery; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = + EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecommendedCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString GuidanceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceReviewPlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPreferReviewRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCoachReview = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPreferShortReviewSet = false; + + bool IsStructurallyValid() const + { + return !TemplateId.IsEmpty(); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingRunState { @@ -1070,6 +1141,9 @@ struct FHyperTwistTrainingRunState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistTrainingImportedGeneratedModeLaunchRequest ImportedGeneratedModeLaunchRequest; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingTemplateLaunchProvenance TemplateLaunchProvenance; + bool IsStructurallyValid() const { return ActiveDeck.IsStructurallyValid() @@ -1390,6 +1464,9 @@ struct FHyperTwistTrainingRunRecord UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistTrainingImportedGeneratedModeLaunchRequest ImportedGeneratedModeLaunchRequest; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingTemplateLaunchProvenance TemplateLaunchProvenance; + bool IsStructurallyValid() const { return Session.IsStructurallyValid() && Summary.TrainingSessionId == Session.TrainingSessionId; @@ -3567,16 +3644,6 @@ struct FHyperTwistTrainingLearnerPreset bool bPreferShortReviewSet = false; }; -UENUM(BlueprintType) -enum class EHyperTwistTrainingSessionTemplateKind : uint8 -{ - ReviewPlan UMETA(DisplayName = "Review Plan"), - Recovery UMETA(DisplayName = "Recovery"), - Accuracy UMETA(DisplayName = "Accuracy"), - Speed UMETA(DisplayName = "Speed"), - Coach UMETA(DisplayName = "Coach") -}; - USTRUCT(BlueprintType) struct FHyperTwistTrainingSessionTemplate { diff --git a/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_REPOSITORY_PROVENANCE_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_REPOSITORY_PROVENANCE_PACKET_2026-05-06.md new file mode 100644 index 0000000..fa2b7e3 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_REPOSITORY_PROVENANCE_PACKET_2026-05-06.md @@ -0,0 +1,105 @@ +# HyperTwist Phase 4 template launch repository provenance packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` repository-backed template provenance slice + +## Purpose + +This packet moves template-launch provenance out of dashboard-only retained state and into repository-backed run state and run history. + +The open tasks were: + +- persist template launch identity into the active run state itself +- carry that provenance into stored run records +- let the dashboard rebuild template-launch recap/history from repository state after widget refresh or rebuild + +It is not: + +- a repository schema migration packet +- a template browser or editor packet +- a coach weighting rewrite packet +- a broader run-history redesign + +## Scope + +Bounded lane: + +- add a compact persisted template-launch provenance struct in training types +- store that provenance on `FHyperTwistTrainingRunState` and `FHyperTwistTrainingRunRecord` +- annotate template-backed subsystem launches so direct template runs and review-plan resume runs both persist provenance +- copy persisted provenance into run records during repository recording +- rebuild dashboard template-launch recap/history from repository run records instead of widget-only retained snapshots + +Out of scope: + +- changing dashboard template ranking +- changing review-plan scoring or recommendation rules +- widening into analytics consumers beyond existing dashboard history/recap +- changing non-template run persistence + +## Why this was the right next packet + +The previous packet made template launch provenance visible on the dashboard, but only as retained widget state. + +That left an obvious continuity gap: + +- widget rebuilds could discard template-launch identity +- dashboard history depended on the current widget lifetime instead of durable run records +- later coach weighting or analytics work still had no repository-backed template launch provenance to read + +So the next honest move was: + +- persist the launch provenance at the run-state layer +- let run-record recording carry it forward automatically +- and switch the dashboard history surface to read from repository state instead of only its own memory + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `FHyperTwistTrainingTemplateLaunchProvenance` + - added persisted template-launch provenance fields to `FHyperTwistTrainingRunState` and `FHyperTwistTrainingRunRecord` + - moved `EHyperTwistTrainingSessionTemplateKind` earlier so the persisted provenance struct can use it +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp` + - `BuildRunRecord(...)` now copies template-launch provenance from active run state into stored run records +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp` + - added shared template-to-provenance projection helpers + - `StartTrainingRunFromTemplate(...)` now persists template-launch provenance for direct launches and stored review-plan resume launches + - `StartActiveMethodDrillFollowUpRun(...)` now persists the same provenance for template-backed follow-up launches +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h` + - switched active/retained template launch cache members to the persisted provenance shape + - replaced widget-only template-history recording with repository-sync plumbing +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - rehydrates active template-launch context from `CachedRunState.TemplateLaunchProvenance` + - rebuilds `[Template Launch History]` from stored run records for the selected user + - keeps recap rendering on the same provenance fields, now backed by repository state + +## Product effect + +Template-backed run provenance now survives beyond a single dashboard widget instance: + +- active and completed template-backed runs carry `TemplateId`, `TemplateKind`, and `SourceReviewPlanId` in repository-backed run state/history +- dashboard template-launch history can be rebuilt after widget refresh or reconstruction +- later analytics or coach-weighting work can read template-launch provenance from stored run records instead of depending on transient UI memory + +## Acceptance criteria + +- template-backed launches persist provenance into active run state +- stored run records retain that provenance +- dashboard recap can rehydrate template launch context from cached repository-backed run state +- dashboard template-launch history rebuilds from repository run records +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. launch a template-backed run and confirm the active run state carries template provenance +3. complete or abort the run and confirm the dashboard template-launch history still appears after widget refresh/rebuild +4. confirm stored run records include template provenance for template-backed launches + +That is the packet.