Persist template launch provenance in run history

This commit is contained in:
axiomlogicnexus 2026-05-06 23:40:31 +02:00
parent b2ca2957fe
commit c17bfca008
6 changed files with 467 additions and 76 deletions

View file

@ -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<FHyperTwistTrainingRunRecord> UserRunRecords =
UHyperTwistTrainingRepositoryLibrary::ListRunRecordsForDeck(
RepositoryState,
HistoryUserId,
FString());
TArray<FHyperTwistCoachDashboardTemplateLaunchHistoryEntry> 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

View file

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

View file

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

View file

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

View file

@ -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<FString> 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
{

View file

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