From c6a88aa9ebca0d41a0474f798429454ea4b77b3f Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 29 Jun 2026 23:43:47 +0000 Subject: [PATCH] Split HyperTwist coach workflow repository family --- .../HyperTwistTrainingRepositoryLibrary.cpp | 887 ----------------- ...TrainingRepositoryLibraryCoachWorkflow.cpp | 892 ++++++++++++++++++ ...erTwistTrainingRepositoryLibraryInternal.h | 44 + ...RTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md | 46 + .../HyperTwist/DEVELOPMENT.md | 45 + 5 files changed, 1027 insertions(+), 887 deletions(-) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index b5679c3..6977212 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -23081,893 +23081,6 @@ TArray UHyperTwistTrainingRepositoryLibrary: return EffectiveTemplates; } -FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( - const FHyperTwistCoachBrief& CoachBrief, - const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, - const FString& PlanId, - const FString& ReferenceUtc, - const int32 RequestedLaunchCaseCount, - const int32 PlannedLaunchCaseCount, - const FString& LaunchBudgetReasonLine, - const TArray& LaunchedCaseIds -) -{ - FHyperTwistTrainingCoachActionPlan ActionPlan; - if (CoachBrief.UserId.IsEmpty() || PlanId.IsEmpty()) - { - return ActionPlan; - } - - ActionPlan.PlanId = PlanId; - ActionPlan.UserId = CoachBrief.UserId; - ActionPlan.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - !ReferenceUtc.IsEmpty() ? ReferenceUtc : CoachBrief.ReferenceUtc - ); - ActionPlan.SourceSessionId = CoachBrief.TrainingSessionId; - ActionPlan.SourceReviewPlanId = CoachBrief.SourceReviewPlanId; - ActionPlan.ParentPlanId = CoachBrief.CarryForwardPlanId; - ActionPlan.RootPlanId = !CoachBrief.RootPlanId.IsEmpty() - ? CoachBrief.RootPlanId - : (!CoachBrief.CarryForwardPlanId.IsEmpty() ? CoachBrief.CarryForwardPlanId : PlanId); - ActionPlan.FocusDeckId = CoachBrief.FocusDeckId; - ActionPlan.FocusMethodSegmentId = CoachBrief.FocusMethodSegmentId; - ActionPlan.FocusLaneLabel = StaticEnum()->GetNameStringByValue( - static_cast(CoachBrief.FocusLane) - ); - ActionPlan.PriorityLabel = StaticEnum()->GetNameStringByValue( - static_cast(CoachBrief.Priority) - ); - ActionPlan.PriorityScore = CoachBrief.PriorityScore; - ActionPlan.Headline = CoachBrief.Headline; - ActionPlan.PrimaryActionLabel = CoachBrief.PrimaryActionLabel; - ActionPlan.SuggestedMode = CoachBrief.SuggestedMode; - ActionPlan.SuggestedSelectionPolicy = CoachBrief.SuggestedSelectionPolicy; - HyperTwistTrainingRepositoryLibraryInternal::ResolveCoachActionPlanPreferenceDefaults( - CoachBrief, - CoachMemorySnapshot, - ActionPlan.SuggestedMode, - ActionPlan.SuggestedSelectionPolicy - ); - ActionPlan.PreferredGuidanceLane = CoachMemorySnapshot.PreferredGuidanceLane; - ActionPlan.PreferredGuidanceSourceLabel = CoachMemorySnapshot.PreferredGuidanceSourceLabel; - ActionPlan.PreferredCoachHandoffKind = CoachMemorySnapshot.PreferredCoachHandoffKind; - ActionPlan.PreferredCoachHandoffSourceLabel = CoachMemorySnapshot.PreferredCoachHandoffSourceLabel; - ActionPlan.RequestedLaunchCaseCount = RequestedLaunchCaseCount > 0 - ? RequestedLaunchCaseCount - : FMath::Max(1, CoachBrief.FocusCaseIds.Num()); - ActionPlan.PlannedLaunchCaseCount = PlannedLaunchCaseCount > 0 - ? PlannedLaunchCaseCount - : ActionPlan.RequestedLaunchCaseCount; - ActionPlan.FocusCaseIds = HyperTwistTrainingRepositoryLibraryInternal::ResolveCoachActionPlanFocusCaseIds( - CoachBrief, - LaunchedCaseIds, - ActionPlan.PlannedLaunchCaseCount - ); - if (ActionPlan.FocusCaseIds.Num() > 0) - { - ActionPlan.PlannedLaunchCaseCount = ActionPlan.FocusCaseIds.Num(); - } - for (const FString& OriginalFocusCaseId : HyperTwistTrainingRepositoryLibraryInternal::DeduplicateCaseIds( - CoachBrief.FocusCaseIds)) - { - if (!ActionPlan.FocusCaseIds.Contains(OriginalFocusCaseId)) - { - ActionPlan.DeferredLaunchCaseIds.Add(OriginalFocusCaseId); - } - } - ActionPlan.LaunchBudgetReasonLine = LaunchBudgetReasonLine; - ActionPlan.bBroadenedLaunchBudget = - ActionPlan.PlannedLaunchCaseCount > ActionPlan.RequestedLaunchCaseCount - || HyperTwistTrainingRepositoryLibraryInternal::ContainsLaunchBudgetBroadeningHint( - ActionPlan.LaunchBudgetReasonLine); - ActionPlan.bTightenedLaunchBudget = - ActionPlan.PlannedLaunchCaseCount < ActionPlan.RequestedLaunchCaseCount - || HyperTwistTrainingRepositoryLibraryInternal::ContainsLaunchBudgetTighteningHint( - ActionPlan.LaunchBudgetReasonLine); - if (ActionPlan.FocusCaseIds.Num() == 0) - { - ActionPlan.FocusCaseIds = CoachBrief.FocusCaseIds; - } - ActionPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Draft; - ActionPlan.FollowUpDepth = FMath::Max(0, CoachBrief.FollowUpDepth); - ActionPlan.CreatedAtUtc = ActionPlan.ReferenceUtc; - ActionPlan.bNeedsCoachReview = CoachBrief.bNeedsCoachReview; - ActionPlan.bNeedsCadenceRecovery = CoachBrief.bNeedsCadenceRecovery; - ActionPlan.bNeedsVarietyRotation = CoachBrief.bNeedsVarietyRotation; - ActionPlan.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; - - for (int32 Index = 0; Index < ActionPlan.FocusCaseIds.Num(); ++Index) - { - FHyperTwistTrainingCoachActionPlanEntry Entry; - Entry.ActionId = FString::Printf(TEXT("%s_entry_%02d"), *PlanId, Index + 1); - Entry.CaseId = ActionPlan.FocusCaseIds[Index]; - Entry.DeckId = CoachBrief.FocusDeckId; - Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; - Entry.ActionLabel = CoachBrief.PrimaryActionLabel; - Entry.Headline = CoachBrief.Headline; - Entry.PriorityScore = CoachBrief.PriorityScore; - Entry.SuggestedMode = ActionPlan.SuggestedMode; - Entry.SuggestedSelectionPolicy = ActionPlan.SuggestedSelectionPolicy; - Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; - if (Entry.IsStructurallyValid()) - { - ActionPlan.Entries.Add(Entry); - } - } - - if (ActionPlan.Entries.Num() == 0) - { - FHyperTwistTrainingCoachActionPlanEntry Entry; - Entry.ActionId = FString::Printf(TEXT("%s_entry_01"), *PlanId); - Entry.CaseId = TEXT("coach-generic"); - Entry.DeckId = CoachBrief.FocusDeckId; - Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; - Entry.ActionLabel = CoachBrief.PrimaryActionLabel.IsEmpty() - ? TEXT("coach-focus") - : CoachBrief.PrimaryActionLabel; - Entry.Headline = CoachBrief.Headline; - Entry.PriorityScore = CoachBrief.PriorityScore; - Entry.SuggestedMode = ActionPlan.SuggestedMode; - Entry.SuggestedSelectionPolicy = ActionPlan.SuggestedSelectionPolicy; - Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; - ActionPlan.Entries.Add(Entry); - } - - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); -} - -FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( - const FHyperTwistTrainingCoachActionPlan& ActionPlan, - const FString& TrainingSessionId, - const FString& StartedAtUtc -) -{ - FHyperTwistTrainingCoachActionPlan UpdatedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); - if (!UpdatedPlan.IsStructurallyValid()) - { - return UpdatedPlan; - } - - UpdatedPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Active; - UpdatedPlan.SourceSessionId = !TrainingSessionId.IsEmpty() ? TrainingSessionId : UpdatedPlan.SourceSessionId; - UpdatedPlan.StartedAtUtc = !StartedAtUtc.IsEmpty() - ? StartedAtUtc - : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); - return UpdatedPlan; -} - -FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::FinalizeCoachActionPlan( - const FHyperTwistTrainingCoachActionPlan& ActionPlan, - const FHyperTwistTrainingSession& Session, - bool bAbortPlan -) -{ - FHyperTwistTrainingCoachActionPlan UpdatedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); - if (!UpdatedPlan.IsStructurallyValid()) - { - return UpdatedPlan; - } - - UpdatedPlan.SourceSessionId = !Session.TrainingSessionId.IsEmpty() - ? Session.TrainingSessionId - : UpdatedPlan.SourceSessionId; - UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty() - ? Session.EndedAtUtc - : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); - UpdatedPlan.PlanState = bAbortPlan || Session.SessionState == EHyperTwistTrainingSessionState::Aborted - ? EHyperTwistTrainingCoachActionPlanState::Aborted - : EHyperTwistTrainingCoachActionPlanState::Completed; - if (UpdatedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed) - { - for (FHyperTwistTrainingCoachActionPlanEntry& Entry : UpdatedPlan.Entries) - { - Entry.bCompleted = true; - } - } - - return UpdatedPlan; -} - -TArray UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId -) -{ - TArray ActionPlans; - if (UserId.IsEmpty()) - { - return ActionPlans; - } - - for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans) - { - if (ActionPlan.UserId == UserId && ActionPlan.IsStructurallyValid()) - { - ActionPlans.Add(ActionPlan); - } - } - - ActionPlans.Sort([](const FHyperTwistTrainingCoachActionPlan& Left, const FHyperTwistTrainingCoachActionPlan& Right) - { - const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( - Left.CreatedAtUtc, - Right.CreatedAtUtc - ); - if (UtcCompare != 0) - { - return UtcCompare > 0; - } - - return Left.PlanId < Right.PlanId; - }); - return ActionPlans; -} - -bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachActionPlan( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& PlanId, - FHyperTwistTrainingCoachActionPlan& OutActionPlan -) -{ - OutActionPlan = FHyperTwistTrainingCoachActionPlan(); - if (PlanId.IsEmpty()) - { - return false; - } - - const FHyperTwistTrainingCoachActionPlan* MatchingPlan = - RepositoryState.StoredCoachActionPlans.FindByPredicate( - [&PlanId](const FHyperTwistTrainingCoachActionPlan& Candidate) - { - return Candidate.PlanId == PlanId && Candidate.IsStructurallyValid(); - } - ); - if (MatchingPlan == nullptr) - { - return false; - } - - OutActionPlan = *MatchingPlan; - return true; -} - -FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachActionPlan& ActionPlan -) -{ - FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); - if (!NormalizedPlan.IsStructurallyValid()) - { - return UpdatedRepositoryState; - } - - FHyperTwistTrainingCoachActionPlan* ExistingPlan = - UpdatedRepositoryState.StoredCoachActionPlans.FindByPredicate( - [&NormalizedPlan](const FHyperTwistTrainingCoachActionPlan& Candidate) - { - return Candidate.UserId == NormalizedPlan.UserId - && Candidate.PlanId == NormalizedPlan.PlanId; - } - ); - if (ExistingPlan != nullptr) - { - *ExistingPlan = NormalizedPlan; - } - else - { - UpdatedRepositoryState.StoredCoachActionPlans.Add(NormalizedPlan); - } - - return UpdatedRepositoryState; -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( - const FHyperTwistCoachSessionQueueSummary& QueueSummary, - const FString& QueueId, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachSessionQueueState QueueState; - if (!QueueSummary.IsStructurallyValid()) - { - return QueueState; - } - - QueueState.QueueId = QueueId.IsEmpty() ? TEXT("coach_queue_active") : QueueId; - QueueState.UserId = QueueSummary.UserId; - QueueState.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - ReferenceUtc.IsEmpty() ? QueueSummary.ReferenceUtc : ReferenceUtc - ); - QueueState.LastUpdatedAtUtc = QueueState.ReferenceUtc; - QueueState.FocusDeckId = QueueSummary.FocusDeckId; - QueueState.FocusMethodSegmentId = QueueSummary.FocusMethodSegmentId; - QueueState.PrimaryQueueLabel = QueueSummary.PrimaryQueueLabel; - QueueState.NextQueueLabel = QueueSummary.NextQueueLabel; - QueueState.bHasImmediateQueue = QueueSummary.bHasImmediateQueue; - QueueState.bNeedsArchiveAwareSequencing = QueueSummary.bNeedsArchiveAwareSequencing; - - for (const FHyperTwistCoachSessionQueueEntry& SummaryEntry : QueueSummary.Entries) - { - FHyperTwistTrainingCoachSessionQueueStateEntry QueueEntry; - QueueEntry.EntryId = SummaryEntry.EntryId; - QueueEntry.SourceLabel = SummaryEntry.SourceLabel; - QueueEntry.SourceBriefTrainingSessionId = SummaryEntry.SourceBriefTrainingSessionId; - QueueEntry.SourceReviewPlanId = SummaryEntry.SourceReviewPlanId; - QueueEntry.CarryForwardPlanId = SummaryEntry.CarryForwardPlanId; - QueueEntry.RootPlanId = SummaryEntry.RootPlanId; - QueueEntry.FollowUpDepth = FMath::Max(0, SummaryEntry.FollowUpDepth); - QueueEntry.SlotIndex = SummaryEntry.SlotIndex; - QueueEntry.PriorityLabel = StaticEnum()->GetNameStringByValue( - static_cast(SummaryEntry.Priority) - ); - QueueEntry.PriorityScore = SummaryEntry.PriorityScore; - QueueEntry.FocusLaneLabel = StaticEnum()->GetNameStringByValue( - static_cast(SummaryEntry.FocusLane) - ); - QueueEntry.Headline = SummaryEntry.Headline; - QueueEntry.PrimaryActionLabel = SummaryEntry.PrimaryActionLabel; - QueueEntry.SuggestedMode = SummaryEntry.SuggestedMode; - QueueEntry.SuggestedSelectionPolicy = SummaryEntry.SuggestedSelectionPolicy; - QueueEntry.FocusDeckId = SummaryEntry.FocusDeckId; - QueueEntry.FocusMethodSegmentId = SummaryEntry.FocusMethodSegmentId; - QueueEntry.FocusCaseIds = SummaryEntry.FocusCaseIds; - QueueEntry.DeferredLaunchCaseCount = FMath::Max(0, SummaryEntry.DeferredLaunchCaseCount); - QueueEntry.bNeedsDeferredLaunchRecovery = SummaryEntry.bNeedsDeferredLaunchRecovery; - QueueEntry.bHasTightenedLaunchOverflow = SummaryEntry.bHasTightenedLaunchOverflow; - QueueEntry.bRequiresFollowUp = SummaryEntry.bRequiresFollowUp; - QueueEntry.bRequiresArchiveRetentionTuning = SummaryEntry.bRequiresArchiveRetentionTuning; - QueueEntry.bNeedsCoachReview = SummaryEntry.bNeedsCoachReview; - QueueEntry.bNeedsCadenceRecovery = SummaryEntry.bNeedsCadenceRecovery; - QueueEntry.bNeedsVarietyRotation = SummaryEntry.bNeedsVarietyRotation; - QueueEntry.bPreferRecognitionReplayReview = SummaryEntry.bPreferRecognitionReplayReview; - QueueEntry.bPinned = QueueEntry.bRequiresFollowUp || QueueEntry.bRequiresArchiveRetentionTuning; - QueueEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; - QueueEntry.WorkFingerprint = - HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueEntryFingerprint(QueueEntry); - QueueState.Entries.Add(QueueEntry); - } - - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState( - const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState, - const FHyperTwistCoachSessionQueueSummary& QueueSummary, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachSessionQueueState ResolvedQueueState = BuildCoachSessionQueueState( - QueueSummary, - ExistingQueueState.QueueId, - ReferenceUtc - ); - if (!ResolvedQueueState.IsStructurallyValid() || !ExistingQueueState.IsStructurallyValid()) - { - return ResolvedQueueState; - } - - for (FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry : ResolvedQueueState.Entries) - { - const FHyperTwistTrainingCoachSessionQueueStateEntry* ExistingEntry = - ExistingQueueState.Entries.FindByPredicate( - [&QueueEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - return Candidate.EntryId == QueueEntry.EntryId - && Candidate.WorkFingerprint == QueueEntry.WorkFingerprint - && Candidate.IsStructurallyValid(); - } - ); - if (ExistingEntry == nullptr) - { - continue; - } - - QueueEntry.EntryState = ExistingEntry->EntryState; - QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId; - QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc; - QueueEntry.DeferredUntilUtc = ExistingEntry->DeferredUntilUtc; - QueueEntry.DeferredAtUtc = ExistingEntry->DeferredAtUtc; - QueueEntry.DeferralReasonLabel = ExistingEntry->DeferralReasonLabel; - QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc; - QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc; - QueueEntry.RescheduleCount = ExistingEntry->RescheduleCount; - QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned; - QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately; - QueueEntry.bWasManuallyRescheduled = ExistingEntry->bWasManuallyRescheduled; - } - - ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - ReferenceUtc.IsEmpty() ? ResolvedQueueState.ReferenceUtc : ReferenceUtc - ); - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(ResolvedQueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( - const FHyperTwistTrainingCoachSessionQueueState& QueueState, - const FString& SourceLabel, - const FString& TrainingSessionId, - const FString& StartedAtUtc, - const FString& StartActionSourceLabel -) -{ - FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!UpdatedQueueState.IsStructurallyValid() || SourceLabel.IsEmpty()) - { - return UpdatedQueueState; - } - - const FString ResolvedStartedAtUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); - int32 ActivatedIndex = INDEX_NONE; - for (int32 EntryIndex = 0; EntryIndex < UpdatedQueueState.Entries.Num(); ++EntryIndex) - { - const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex]; - if (Entry.SourceLabel == SourceLabel - && Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred - && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState)) - { - ActivatedIndex = EntryIndex; - break; - } - } - if (ActivatedIndex == INDEX_NONE) - { - return UpdatedQueueState; - } - - for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries) - { - if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) - { - Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; - Entry.SourceTrainingSessionId.Reset(); - Entry.StartActionSourceLabel.Reset(); - Entry.StartedAtUtc.Reset(); - Entry.bCanStartImmediately = false; - } - } - - FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry = - UpdatedQueueState.Entries[ActivatedIndex]; - ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; - ActiveEntry.SourceTrainingSessionId = TrainingSessionId; - ActiveEntry.StartActionSourceLabel = !StartActionSourceLabel.IsEmpty() - ? StartActionSourceLabel - : SourceLabel; - ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() - ? ActiveEntry.ScheduledAtUtc - : ResolvedStartedAtUtc; - ActiveEntry.DeferredUntilUtc.Reset(); - ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; - ActiveEntry.bCanStartImmediately = false; - UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById( - const FHyperTwistTrainingCoachSessionQueueState& QueueState, - const FString& EntryId, - const FString& TrainingSessionId, - const FString& StartedAtUtc -) -{ - FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) - { - return UpdatedQueueState; - } - - const FString ResolvedStartedAtUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); - const int32 ActivatedIndex = UpdatedQueueState.Entries.IndexOfByPredicate( - [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - return Candidate.EntryId == EntryId - && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred - && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); - } - ); - if (ActivatedIndex == INDEX_NONE) - { - return UpdatedQueueState; - } - - for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries) - { - if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) - { - Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; - Entry.SourceTrainingSessionId.Reset(); - Entry.StartedAtUtc.Reset(); - Entry.bCanStartImmediately = false; - } - } - - FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry = - UpdatedQueueState.Entries[ActivatedIndex]; - ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; - ActiveEntry.SourceTrainingSessionId = TrainingSessionId; - ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() - ? ActiveEntry.ScheduledAtUtc - : ResolvedStartedAtUtc; - ActiveEntry.DeferredUntilUtc.Reset(); - ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; - ActiveEntry.bCanStartImmediately = false; - UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( - const FHyperTwistTrainingCoachSessionQueueState& QueueState, - const FString& TrainingSessionId, - const FString& CompletedAtUtc, - bool bAbortEntry -) -{ - FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!UpdatedQueueState.IsStructurallyValid()) - { - return UpdatedQueueState; - } - - const FString ResolvedCompletedAtUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - CompletedAtUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : CompletedAtUtc - ); - FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveEntry = - UpdatedQueueState.Entries.FindByPredicate( - [&TrainingSessionId, &UpdatedQueueState](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - if (!TrainingSessionId.IsEmpty() && Candidate.SourceTrainingSessionId == TrainingSessionId) - { - return true; - } - - return Candidate.EntryId == UpdatedQueueState.ActiveEntryId - && Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; - } - ); - if (ActiveEntry == nullptr) - { - return UpdatedQueueState; - } - - ActiveEntry->EntryState = bAbortEntry - ? EHyperTwistTrainingCoachSessionQueueEntryState::Skipped - : EHyperTwistTrainingCoachSessionQueueEntryState::Completed; - ActiveEntry->CompletedAtUtc = ResolvedCompletedAtUtc; - ActiveEntry->bCanStartImmediately = false; - UpdatedQueueState.ActiveEntryId.Reset(); - UpdatedQueueState.ActiveTrainingSessionId.Reset(); - UpdatedQueueState.LastUpdatedAtUtc = ResolvedCompletedAtUtc; - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( - const FHyperTwistTrainingCoachSessionQueueState& QueueState, - const FString& EntryId, - const FString& DeferredUntilUtc, - const FString& DeferralReasonLabel, - bool bManualReschedule, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty()) - { - return UpdatedQueueState; - } - - const FString ResolvedReferenceUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc - ); - FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToDefer = - UpdatedQueueState.Entries.FindByPredicate( - [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - return Candidate.EntryId == EntryId - && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress - && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); - } - ); - if (EntryToDefer == nullptr) - { - return UpdatedQueueState; - } - - EntryToDefer->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Deferred; - EntryToDefer->DeferredUntilUtc = DeferredUntilUtc; - EntryToDefer->DeferredAtUtc = ResolvedReferenceUtc; - EntryToDefer->DeferralReasonLabel = !DeferralReasonLabel.IsEmpty() - ? DeferralReasonLabel - : (bManualReschedule ? TEXT("manual-reschedule") : TEXT("deferred-window")); - EntryToDefer->SourceTrainingSessionId.Reset(); - EntryToDefer->StartedAtUtc.Reset(); - EntryToDefer->CompletedAtUtc.Reset(); - EntryToDefer->bCanStartImmediately = false; - EntryToDefer->RescheduleCount += 1; - EntryToDefer->bWasManuallyRescheduled = EntryToDefer->bWasManuallyRescheduled || bManualReschedule; - UpdatedQueueState.ActiveEntryId.Reset(); - UpdatedQueueState.ActiveTrainingSessionId.Reset(); - UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); -} - -FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry( - const FHyperTwistTrainingCoachSessionQueueState& QueueState, - const FString& EntryId, - const FString& StartActionSourceLabel, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) - { - return UpdatedQueueState; - } - - const FString ResolvedReferenceUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc - ); - FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToPromote = - UpdatedQueueState.Entries.FindByPredicate( - [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - return Candidate.EntryId == EntryId - && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress - && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); - } - ); - if (EntryToPromote == nullptr) - { - return UpdatedQueueState; - } - - EntryToPromote->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready; - EntryToPromote->ScheduledAtUtc = ResolvedReferenceUtc; - EntryToPromote->DeferredUntilUtc.Reset(); - EntryToPromote->bCanStartImmediately = true; - EntryToPromote->SlotIndex = -1; - if (!StartActionSourceLabel.IsEmpty()) - { - EntryToPromote->StartActionSourceLabel = StartActionSourceLabel; - } - UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; - return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); -} - -TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId -) -{ - TArray QueueStates; - if (UserId.IsEmpty()) - { - return QueueStates; - } - - for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues) - { - if (QueueState.UserId == UserId && QueueState.IsStructurallyValid()) - { - QueueStates.Add(QueueState); - } - } - - QueueStates.Sort([]( - const FHyperTwistTrainingCoachSessionQueueState& Left, - const FHyperTwistTrainingCoachSessionQueueState& Right - ) - { - const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( - Left.LastUpdatedAtUtc, - Right.LastUpdatedAtUtc - ); - if (UtcCompare != 0) - { - return UtcCompare > 0; - } - - return Left.QueueId < Right.QueueId; - }); - return QueueStates; -} - -TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& QueueId -) -{ - TArray HistoryEntries; - if (UserId.IsEmpty() || QueueId.IsEmpty()) - { - return HistoryEntries; - } - - for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory) - { - if (HistoryEntry.UserId == UserId - && HistoryEntry.QueueId == QueueId - && HistoryEntry.IsStructurallyValid()) - { - HistoryEntries.Add(HistoryEntry); - } - } - - HistoryEntries.Sort([]( - const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left, - const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right - ) - { - const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( - Left.EventAtUtc, - Right.EventAtUtc - ); - if (UtcCompare != 0) - { - return UtcCompare < 0; - } - - return Left.HistoryEntryId < Right.HistoryEntryId; - }); - return HistoryEntries; -} - -bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& QueueId, - FHyperTwistTrainingCoachSessionQueueState& OutQueueState -) -{ - OutQueueState = FHyperTwistTrainingCoachSessionQueueState(); - if (UserId.IsEmpty() || QueueId.IsEmpty()) - { - return false; - } - - const FHyperTwistTrainingCoachSessionQueueState* MatchingQueueState = - RepositoryState.StoredCoachSessionQueues.FindByPredicate( - [&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate) - { - return Candidate.UserId == UserId - && Candidate.QueueId == QueueId - && Candidate.IsStructurallyValid(); - } - ); - if (MatchingQueueState == nullptr) - { - return false; - } - - OutQueueState = *MatchingQueueState; - return true; -} - -FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachSessionQueueState& QueueState -) -{ - FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; - const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); - if (!NormalizedQueueState.IsStructurallyValid()) - { - return UpdatedRepositoryState; - } - - const FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState = - UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( - [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) - { - return Candidate.UserId == NormalizedQueueState.UserId - && Candidate.QueueId == NormalizedQueueState.QueueId; - } - ); - - for (const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry : NormalizedQueueState.Entries) - { - const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry = ExistingQueueState != nullptr - ? ExistingQueueState->Entries.FindByPredicate( - [&CurrentEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) - { - return Candidate.EntryId == CurrentEntry.EntryId; - } - ) - : nullptr; - if (PreviousEntry != nullptr && PreviousEntry->WorkFingerprint != CurrentEntry.WorkFingerprint) - { - PreviousEntry = nullptr; - } - if (PreviousEntry != nullptr - && !HyperTwistTrainingRepositoryLibraryInternal::DidCoachSessionQueueEntryChange( - *PreviousEntry, - CurrentEntry - )) - { - continue; - } - - const FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry = - HyperTwistTrainingRepositoryLibraryInternal::BuildCoachSessionQueueHistoryEntry( - NormalizedQueueState, - CurrentEntry, - PreviousEntry - ); - if (!HistoryEntry.IsStructurallyValid()) - { - continue; - } - - const bool bAlreadyExists = - UpdatedRepositoryState.StoredCoachSessionQueueHistory.ContainsByPredicate( - [&HistoryEntry](const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Candidate) - { - return Candidate.HistoryEntryId == HistoryEntry.HistoryEntryId; - } - ); - if (!bAlreadyExists) - { - UpdatedRepositoryState.StoredCoachSessionQueueHistory.Add(HistoryEntry); - } - } - - FHyperTwistTrainingCoachSessionQueueState* MutableExistingQueueState = - UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( - [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) - { - return Candidate.UserId == NormalizedQueueState.UserId - && Candidate.QueueId == NormalizedQueueState.QueueId; - } - ); - if (MutableExistingQueueState != nullptr) - { - *MutableExistingQueueState = NormalizedQueueState; - } - else - { - UpdatedRepositoryState.StoredCoachSessionQueues.Add(NormalizedQueueState); - } - - return UpdatedRepositoryState; -} - -FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RemoveCoachSessionQueueState( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& QueueId -) -{ - if (UserId.IsEmpty() || QueueId.IsEmpty()) - { - return RepositoryState; - } - - FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; - UpdatedRepositoryState.StoredCoachSessionQueues.RemoveAll( - [&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate) - { - return Candidate.UserId == UserId && Candidate.QueueId == QueueId; - } - ); - return UpdatedRepositoryState; -} - FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary( const FHyperTwistTrainingRepositoryState& RepositoryState, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp new file mode 100644 index 0000000..c4fab39 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp @@ -0,0 +1,892 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" + +#include "HyperTwistTrainingRepositoryLibraryInternal.h" + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + const FHyperTwistCoachBrief& CoachBrief, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FString& PlanId, + const FString& ReferenceUtc, + const int32 RequestedLaunchCaseCount, + const int32 PlannedLaunchCaseCount, + const FString& LaunchBudgetReasonLine, + const TArray& LaunchedCaseIds +) +{ + FHyperTwistTrainingCoachActionPlan ActionPlan; + if (CoachBrief.UserId.IsEmpty() || PlanId.IsEmpty()) + { + return ActionPlan; + } + + ActionPlan.PlanId = PlanId; + ActionPlan.UserId = CoachBrief.UserId; + ActionPlan.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + !ReferenceUtc.IsEmpty() ? ReferenceUtc : CoachBrief.ReferenceUtc + ); + ActionPlan.SourceSessionId = CoachBrief.TrainingSessionId; + ActionPlan.SourceReviewPlanId = CoachBrief.SourceReviewPlanId; + ActionPlan.ParentPlanId = CoachBrief.CarryForwardPlanId; + ActionPlan.RootPlanId = !CoachBrief.RootPlanId.IsEmpty() + ? CoachBrief.RootPlanId + : (!CoachBrief.CarryForwardPlanId.IsEmpty() ? CoachBrief.CarryForwardPlanId : PlanId); + ActionPlan.FocusDeckId = CoachBrief.FocusDeckId; + ActionPlan.FocusMethodSegmentId = CoachBrief.FocusMethodSegmentId; + ActionPlan.FocusLaneLabel = StaticEnum()->GetNameStringByValue( + static_cast(CoachBrief.FocusLane) + ); + ActionPlan.PriorityLabel = StaticEnum()->GetNameStringByValue( + static_cast(CoachBrief.Priority) + ); + ActionPlan.PriorityScore = CoachBrief.PriorityScore; + ActionPlan.Headline = CoachBrief.Headline; + ActionPlan.PrimaryActionLabel = CoachBrief.PrimaryActionLabel; + ActionPlan.SuggestedMode = CoachBrief.SuggestedMode; + ActionPlan.SuggestedSelectionPolicy = CoachBrief.SuggestedSelectionPolicy; + HyperTwistTrainingRepositoryLibraryInternal::ResolveCoachActionPlanPreferenceDefaults( + CoachBrief, + CoachMemorySnapshot, + ActionPlan.SuggestedMode, + ActionPlan.SuggestedSelectionPolicy + ); + ActionPlan.PreferredGuidanceLane = CoachMemorySnapshot.PreferredGuidanceLane; + ActionPlan.PreferredGuidanceSourceLabel = CoachMemorySnapshot.PreferredGuidanceSourceLabel; + ActionPlan.PreferredCoachHandoffKind = CoachMemorySnapshot.PreferredCoachHandoffKind; + ActionPlan.PreferredCoachHandoffSourceLabel = CoachMemorySnapshot.PreferredCoachHandoffSourceLabel; + ActionPlan.RequestedLaunchCaseCount = RequestedLaunchCaseCount > 0 + ? RequestedLaunchCaseCount + : FMath::Max(1, CoachBrief.FocusCaseIds.Num()); + ActionPlan.PlannedLaunchCaseCount = PlannedLaunchCaseCount > 0 + ? PlannedLaunchCaseCount + : ActionPlan.RequestedLaunchCaseCount; + ActionPlan.FocusCaseIds = HyperTwistTrainingRepositoryLibraryInternal::ResolveCoachActionPlanFocusCaseIds( + CoachBrief, + LaunchedCaseIds, + ActionPlan.PlannedLaunchCaseCount + ); + if (ActionPlan.FocusCaseIds.Num() > 0) + { + ActionPlan.PlannedLaunchCaseCount = ActionPlan.FocusCaseIds.Num(); + } + for (const FString& OriginalFocusCaseId : HyperTwistTrainingRepositoryLibraryInternal::DeduplicateCaseIds( + CoachBrief.FocusCaseIds)) + { + if (!ActionPlan.FocusCaseIds.Contains(OriginalFocusCaseId)) + { + ActionPlan.DeferredLaunchCaseIds.Add(OriginalFocusCaseId); + } + } + ActionPlan.LaunchBudgetReasonLine = LaunchBudgetReasonLine; + ActionPlan.bBroadenedLaunchBudget = + ActionPlan.PlannedLaunchCaseCount > ActionPlan.RequestedLaunchCaseCount + || HyperTwistTrainingRepositoryLibraryInternal::ContainsLaunchBudgetBroadeningHint( + ActionPlan.LaunchBudgetReasonLine); + ActionPlan.bTightenedLaunchBudget = + ActionPlan.PlannedLaunchCaseCount < ActionPlan.RequestedLaunchCaseCount + || HyperTwistTrainingRepositoryLibraryInternal::ContainsLaunchBudgetTighteningHint( + ActionPlan.LaunchBudgetReasonLine); + if (ActionPlan.FocusCaseIds.Num() == 0) + { + ActionPlan.FocusCaseIds = CoachBrief.FocusCaseIds; + } + ActionPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Draft; + ActionPlan.FollowUpDepth = FMath::Max(0, CoachBrief.FollowUpDepth); + ActionPlan.CreatedAtUtc = ActionPlan.ReferenceUtc; + ActionPlan.bNeedsCoachReview = CoachBrief.bNeedsCoachReview; + ActionPlan.bNeedsCadenceRecovery = CoachBrief.bNeedsCadenceRecovery; + ActionPlan.bNeedsVarietyRotation = CoachBrief.bNeedsVarietyRotation; + ActionPlan.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + + for (int32 Index = 0; Index < ActionPlan.FocusCaseIds.Num(); ++Index) + { + FHyperTwistTrainingCoachActionPlanEntry Entry; + Entry.ActionId = FString::Printf(TEXT("%s_entry_%02d"), *PlanId, Index + 1); + Entry.CaseId = ActionPlan.FocusCaseIds[Index]; + Entry.DeckId = CoachBrief.FocusDeckId; + Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; + Entry.ActionLabel = CoachBrief.PrimaryActionLabel; + Entry.Headline = CoachBrief.Headline; + Entry.PriorityScore = CoachBrief.PriorityScore; + Entry.SuggestedMode = ActionPlan.SuggestedMode; + Entry.SuggestedSelectionPolicy = ActionPlan.SuggestedSelectionPolicy; + Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + if (Entry.IsStructurallyValid()) + { + ActionPlan.Entries.Add(Entry); + } + } + + if (ActionPlan.Entries.Num() == 0) + { + FHyperTwistTrainingCoachActionPlanEntry Entry; + Entry.ActionId = FString::Printf(TEXT("%s_entry_01"), *PlanId); + Entry.CaseId = TEXT("coach-generic"); + Entry.DeckId = CoachBrief.FocusDeckId; + Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; + Entry.ActionLabel = CoachBrief.PrimaryActionLabel.IsEmpty() + ? TEXT("coach-focus") + : CoachBrief.PrimaryActionLabel; + Entry.Headline = CoachBrief.Headline; + Entry.PriorityScore = CoachBrief.PriorityScore; + Entry.SuggestedMode = ActionPlan.SuggestedMode; + Entry.SuggestedSelectionPolicy = ActionPlan.SuggestedSelectionPolicy; + Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + ActionPlan.Entries.Add(Entry); + } + + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& TrainingSessionId, + const FString& StartedAtUtc +) +{ + FHyperTwistTrainingCoachActionPlan UpdatedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!UpdatedPlan.IsStructurallyValid()) + { + return UpdatedPlan; + } + + UpdatedPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Active; + UpdatedPlan.SourceSessionId = !TrainingSessionId.IsEmpty() ? TrainingSessionId : UpdatedPlan.SourceSessionId; + UpdatedPlan.StartedAtUtc = !StartedAtUtc.IsEmpty() + ? StartedAtUtc + : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); + return UpdatedPlan; +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::FinalizeCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FHyperTwistTrainingSession& Session, + bool bAbortPlan +) +{ + FHyperTwistTrainingCoachActionPlan UpdatedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!UpdatedPlan.IsStructurallyValid()) + { + return UpdatedPlan; + } + + UpdatedPlan.SourceSessionId = !Session.TrainingSessionId.IsEmpty() + ? Session.TrainingSessionId + : UpdatedPlan.SourceSessionId; + UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty() + ? Session.EndedAtUtc + : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); + UpdatedPlan.PlanState = bAbortPlan || Session.SessionState == EHyperTwistTrainingSessionState::Aborted + ? EHyperTwistTrainingCoachActionPlanState::Aborted + : EHyperTwistTrainingCoachActionPlanState::Completed; + if (UpdatedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed) + { + for (FHyperTwistTrainingCoachActionPlanEntry& Entry : UpdatedPlan.Entries) + { + Entry.bCompleted = true; + } + } + + return UpdatedPlan; +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId +) +{ + TArray ActionPlans; + if (UserId.IsEmpty()) + { + return ActionPlans; + } + + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans) + { + if (ActionPlan.UserId == UserId && ActionPlan.IsStructurallyValid()) + { + ActionPlans.Add(ActionPlan); + } + } + + ActionPlans.Sort([](const FHyperTwistTrainingCoachActionPlan& Left, const FHyperTwistTrainingCoachActionPlan& Right) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.CreatedAtUtc, + Right.CreatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.PlanId < Right.PlanId; + }); + return ActionPlans; +} + +bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan +) +{ + OutActionPlan = FHyperTwistTrainingCoachActionPlan(); + if (PlanId.IsEmpty()) + { + return false; + } + + const FHyperTwistTrainingCoachActionPlan* MatchingPlan = + RepositoryState.StoredCoachActionPlans.FindByPredicate( + [&PlanId](const FHyperTwistTrainingCoachActionPlan& Candidate) + { + return Candidate.PlanId == PlanId && Candidate.IsStructurallyValid(); + } + ); + if (MatchingPlan == nullptr) + { + return false; + } + + OutActionPlan = *MatchingPlan; + return true; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!NormalizedPlan.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + FHyperTwistTrainingCoachActionPlan* ExistingPlan = + UpdatedRepositoryState.StoredCoachActionPlans.FindByPredicate( + [&NormalizedPlan](const FHyperTwistTrainingCoachActionPlan& Candidate) + { + return Candidate.UserId == NormalizedPlan.UserId + && Candidate.PlanId == NormalizedPlan.PlanId; + } + ); + if (ExistingPlan != nullptr) + { + *ExistingPlan = NormalizedPlan; + } + else + { + UpdatedRepositoryState.StoredCoachActionPlans.Add(NormalizedPlan); + } + + return UpdatedRepositoryState; +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& QueueId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState QueueState; + if (!QueueSummary.IsStructurallyValid()) + { + return QueueState; + } + + QueueState.QueueId = QueueId.IsEmpty() ? TEXT("coach_queue_active") : QueueId; + QueueState.UserId = QueueSummary.UserId; + QueueState.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? QueueSummary.ReferenceUtc : ReferenceUtc + ); + QueueState.LastUpdatedAtUtc = QueueState.ReferenceUtc; + QueueState.FocusDeckId = QueueSummary.FocusDeckId; + QueueState.FocusMethodSegmentId = QueueSummary.FocusMethodSegmentId; + QueueState.PrimaryQueueLabel = QueueSummary.PrimaryQueueLabel; + QueueState.NextQueueLabel = QueueSummary.NextQueueLabel; + QueueState.bHasImmediateQueue = QueueSummary.bHasImmediateQueue; + QueueState.bNeedsArchiveAwareSequencing = QueueSummary.bNeedsArchiveAwareSequencing; + + for (const FHyperTwistCoachSessionQueueEntry& SummaryEntry : QueueSummary.Entries) + { + FHyperTwistTrainingCoachSessionQueueStateEntry QueueEntry; + QueueEntry.EntryId = SummaryEntry.EntryId; + QueueEntry.SourceLabel = SummaryEntry.SourceLabel; + QueueEntry.SourceBriefTrainingSessionId = SummaryEntry.SourceBriefTrainingSessionId; + QueueEntry.SourceReviewPlanId = SummaryEntry.SourceReviewPlanId; + QueueEntry.CarryForwardPlanId = SummaryEntry.CarryForwardPlanId; + QueueEntry.RootPlanId = SummaryEntry.RootPlanId; + QueueEntry.FollowUpDepth = FMath::Max(0, SummaryEntry.FollowUpDepth); + QueueEntry.SlotIndex = SummaryEntry.SlotIndex; + QueueEntry.PriorityLabel = StaticEnum()->GetNameStringByValue( + static_cast(SummaryEntry.Priority) + ); + QueueEntry.PriorityScore = SummaryEntry.PriorityScore; + QueueEntry.FocusLaneLabel = StaticEnum()->GetNameStringByValue( + static_cast(SummaryEntry.FocusLane) + ); + QueueEntry.Headline = SummaryEntry.Headline; + QueueEntry.PrimaryActionLabel = SummaryEntry.PrimaryActionLabel; + QueueEntry.SuggestedMode = SummaryEntry.SuggestedMode; + QueueEntry.SuggestedSelectionPolicy = SummaryEntry.SuggestedSelectionPolicy; + QueueEntry.FocusDeckId = SummaryEntry.FocusDeckId; + QueueEntry.FocusMethodSegmentId = SummaryEntry.FocusMethodSegmentId; + QueueEntry.FocusCaseIds = SummaryEntry.FocusCaseIds; + QueueEntry.DeferredLaunchCaseCount = FMath::Max(0, SummaryEntry.DeferredLaunchCaseCount); + QueueEntry.bNeedsDeferredLaunchRecovery = SummaryEntry.bNeedsDeferredLaunchRecovery; + QueueEntry.bHasTightenedLaunchOverflow = SummaryEntry.bHasTightenedLaunchOverflow; + QueueEntry.bRequiresFollowUp = SummaryEntry.bRequiresFollowUp; + QueueEntry.bRequiresArchiveRetentionTuning = SummaryEntry.bRequiresArchiveRetentionTuning; + QueueEntry.bNeedsCoachReview = SummaryEntry.bNeedsCoachReview; + QueueEntry.bNeedsCadenceRecovery = SummaryEntry.bNeedsCadenceRecovery; + QueueEntry.bNeedsVarietyRotation = SummaryEntry.bNeedsVarietyRotation; + QueueEntry.bPreferRecognitionReplayReview = SummaryEntry.bPreferRecognitionReplayReview; + QueueEntry.bPinned = QueueEntry.bRequiresFollowUp || QueueEntry.bRequiresArchiveRetentionTuning; + QueueEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + QueueEntry.WorkFingerprint = + HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueEntryFingerprint(QueueEntry); + QueueState.Entries.Add(QueueEntry); + } + + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState, + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState ResolvedQueueState = BuildCoachSessionQueueState( + QueueSummary, + ExistingQueueState.QueueId, + ReferenceUtc + ); + if (!ResolvedQueueState.IsStructurallyValid() || !ExistingQueueState.IsStructurallyValid()) + { + return ResolvedQueueState; + } + + for (FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry : ResolvedQueueState.Entries) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry* ExistingEntry = + ExistingQueueState.Entries.FindByPredicate( + [&QueueEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == QueueEntry.EntryId + && Candidate.WorkFingerprint == QueueEntry.WorkFingerprint + && Candidate.IsStructurallyValid(); + } + ); + if (ExistingEntry == nullptr) + { + continue; + } + + QueueEntry.EntryState = ExistingEntry->EntryState; + QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId; + QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc; + QueueEntry.DeferredUntilUtc = ExistingEntry->DeferredUntilUtc; + QueueEntry.DeferredAtUtc = ExistingEntry->DeferredAtUtc; + QueueEntry.DeferralReasonLabel = ExistingEntry->DeferralReasonLabel; + QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc; + QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc; + QueueEntry.RescheduleCount = ExistingEntry->RescheduleCount; + QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned; + QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately; + QueueEntry.bWasManuallyRescheduled = ExistingEntry->bWasManuallyRescheduled; + } + + ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? ResolvedQueueState.ReferenceUtc : ReferenceUtc + ); + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(ResolvedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& SourceLabel, + const FString& TrainingSessionId, + const FString& StartedAtUtc, + const FString& StartActionSourceLabel +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || SourceLabel.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedStartedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); + int32 ActivatedIndex = INDEX_NONE; + for (int32 EntryIndex = 0; EntryIndex < UpdatedQueueState.Entries.Num(); ++EntryIndex) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex]; + if (Entry.SourceLabel == SourceLabel + && Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState)) + { + ActivatedIndex = EntryIndex; + break; + } + } + if (ActivatedIndex == INDEX_NONE) + { + return UpdatedQueueState; + } + + for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries) + { + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + Entry.SourceTrainingSessionId.Reset(); + Entry.StartActionSourceLabel.Reset(); + Entry.StartedAtUtc.Reset(); + Entry.bCanStartImmediately = false; + } + } + + FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry = + UpdatedQueueState.Entries[ActivatedIndex]; + ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; + ActiveEntry.SourceTrainingSessionId = TrainingSessionId; + ActiveEntry.StartActionSourceLabel = !StartActionSourceLabel.IsEmpty() + ? StartActionSourceLabel + : SourceLabel; + ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() + ? ActiveEntry.ScheduledAtUtc + : ResolvedStartedAtUtc; + ActiveEntry.DeferredUntilUtc.Reset(); + ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; + ActiveEntry.bCanStartImmediately = false; + UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& TrainingSessionId, + const FString& StartedAtUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedStartedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); + const int32 ActivatedIndex = UpdatedQueueState.Entries.IndexOfByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + if (ActivatedIndex == INDEX_NONE) + { + return UpdatedQueueState; + } + + for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries) + { + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + Entry.SourceTrainingSessionId.Reset(); + Entry.StartedAtUtc.Reset(); + Entry.bCanStartImmediately = false; + } + } + + FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry = + UpdatedQueueState.Entries[ActivatedIndex]; + ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; + ActiveEntry.SourceTrainingSessionId = TrainingSessionId; + ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() + ? ActiveEntry.ScheduledAtUtc + : ResolvedStartedAtUtc; + ActiveEntry.DeferredUntilUtc.Reset(); + ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; + ActiveEntry.bCanStartImmediately = false; + UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& TrainingSessionId, + const FString& CompletedAtUtc, + bool bAbortEntry +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid()) + { + return UpdatedQueueState; + } + + const FString ResolvedCompletedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + CompletedAtUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : CompletedAtUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveEntry = + UpdatedQueueState.Entries.FindByPredicate( + [&TrainingSessionId, &UpdatedQueueState](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + if (!TrainingSessionId.IsEmpty() && Candidate.SourceTrainingSessionId == TrainingSessionId) + { + return true; + } + + return Candidate.EntryId == UpdatedQueueState.ActiveEntryId + && Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; + } + ); + if (ActiveEntry == nullptr) + { + return UpdatedQueueState; + } + + ActiveEntry->EntryState = bAbortEntry + ? EHyperTwistTrainingCoachSessionQueueEntryState::Skipped + : EHyperTwistTrainingCoachSessionQueueEntryState::Completed; + ActiveEntry->CompletedAtUtc = ResolvedCompletedAtUtc; + ActiveEntry->bCanStartImmediately = false; + UpdatedQueueState.ActiveEntryId.Reset(); + UpdatedQueueState.ActiveTrainingSessionId.Reset(); + UpdatedQueueState.LastUpdatedAtUtc = ResolvedCompletedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedReferenceUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToDefer = + UpdatedQueueState.Entries.FindByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + if (EntryToDefer == nullptr) + { + return UpdatedQueueState; + } + + EntryToDefer->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Deferred; + EntryToDefer->DeferredUntilUtc = DeferredUntilUtc; + EntryToDefer->DeferredAtUtc = ResolvedReferenceUtc; + EntryToDefer->DeferralReasonLabel = !DeferralReasonLabel.IsEmpty() + ? DeferralReasonLabel + : (bManualReschedule ? TEXT("manual-reschedule") : TEXT("deferred-window")); + EntryToDefer->SourceTrainingSessionId.Reset(); + EntryToDefer->StartedAtUtc.Reset(); + EntryToDefer->CompletedAtUtc.Reset(); + EntryToDefer->bCanStartImmediately = false; + EntryToDefer->RescheduleCount += 1; + EntryToDefer->bWasManuallyRescheduled = EntryToDefer->bWasManuallyRescheduled || bManualReschedule; + UpdatedQueueState.ActiveEntryId.Reset(); + UpdatedQueueState.ActiveTrainingSessionId.Reset(); + UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& StartActionSourceLabel, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedReferenceUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToPromote = + UpdatedQueueState.Entries.FindByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + if (EntryToPromote == nullptr) + { + return UpdatedQueueState; + } + + EntryToPromote->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready; + EntryToPromote->ScheduledAtUtc = ResolvedReferenceUtc; + EntryToPromote->DeferredUntilUtc.Reset(); + EntryToPromote->bCanStartImmediately = true; + EntryToPromote->SlotIndex = -1; + if (!StartActionSourceLabel.IsEmpty()) + { + EntryToPromote->StartActionSourceLabel = StartActionSourceLabel; + } + UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId +) +{ + TArray QueueStates; + if (UserId.IsEmpty()) + { + return QueueStates; + } + + for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues) + { + if (QueueState.UserId == UserId && QueueState.IsStructurallyValid()) + { + QueueStates.Add(QueueState); + } + } + + QueueStates.Sort([]( + const FHyperTwistTrainingCoachSessionQueueState& Left, + const FHyperTwistTrainingCoachSessionQueueState& Right + ) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.LastUpdatedAtUtc, + Right.LastUpdatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.QueueId < Right.QueueId; + }); + return QueueStates; +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId +) +{ + TArray HistoryEntries; + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return HistoryEntries; + } + + for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory) + { + if (HistoryEntry.UserId == UserId + && HistoryEntry.QueueId == QueueId + && HistoryEntry.IsStructurallyValid()) + { + HistoryEntries.Add(HistoryEntry); + } + } + + HistoryEntries.Sort([]( + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left, + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right + ) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.EventAtUtc, + Right.EventAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare < 0; + } + + return Left.HistoryEntryId < Right.HistoryEntryId; + }); + return HistoryEntries; +} + +bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState +) +{ + OutQueueState = FHyperTwistTrainingCoachSessionQueueState(); + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return false; + } + + const FHyperTwistTrainingCoachSessionQueueState* MatchingQueueState = + RepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == UserId + && Candidate.QueueId == QueueId + && Candidate.IsStructurallyValid(); + } + ); + if (MatchingQueueState == nullptr) + { + return false; + } + + OutQueueState = *MatchingQueueState; + return true; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachSessionQueueState& QueueState +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!NormalizedQueueState.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + const FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState = + UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == NormalizedQueueState.UserId + && Candidate.QueueId == NormalizedQueueState.QueueId; + } + ); + + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry : NormalizedQueueState.Entries) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry = ExistingQueueState != nullptr + ? ExistingQueueState->Entries.FindByPredicate( + [&CurrentEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == CurrentEntry.EntryId; + } + ) + : nullptr; + if (PreviousEntry != nullptr && PreviousEntry->WorkFingerprint != CurrentEntry.WorkFingerprint) + { + PreviousEntry = nullptr; + } + if (PreviousEntry != nullptr + && !HyperTwistTrainingRepositoryLibraryInternal::DidCoachSessionQueueEntryChange( + *PreviousEntry, + CurrentEntry + )) + { + continue; + } + + const FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry = + HyperTwistTrainingRepositoryLibraryInternal::BuildCoachSessionQueueHistoryEntry( + NormalizedQueueState, + CurrentEntry, + PreviousEntry + ); + if (!HistoryEntry.IsStructurallyValid()) + { + continue; + } + + const bool bAlreadyExists = + UpdatedRepositoryState.StoredCoachSessionQueueHistory.ContainsByPredicate( + [&HistoryEntry](const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Candidate) + { + return Candidate.HistoryEntryId == HistoryEntry.HistoryEntryId; + } + ); + if (!bAlreadyExists) + { + UpdatedRepositoryState.StoredCoachSessionQueueHistory.Add(HistoryEntry); + } + } + + FHyperTwistTrainingCoachSessionQueueState* MutableExistingQueueState = + UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == NormalizedQueueState.UserId + && Candidate.QueueId == NormalizedQueueState.QueueId; + } + ); + if (MutableExistingQueueState != nullptr) + { + *MutableExistingQueueState = NormalizedQueueState; + } + else + { + UpdatedRepositoryState.StoredCoachSessionQueues.Add(NormalizedQueueState); + } + + return UpdatedRepositoryState; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RemoveCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId +) +{ + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return RepositoryState; + } + + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + UpdatedRepositoryState.StoredCoachSessionQueues.RemoveAll( + [&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == UserId && Candidate.QueueId == QueueId; + } + ); + return UpdatedRepositoryState; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h index 25fbcbb..9b71753 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h @@ -23,4 +23,48 @@ namespace HyperTwistTrainingRepositoryLibraryInternal const FHyperTwistTrainingReviewPolicy& ReviewPolicy, int32 RequestedMaxCases ); + + bool ContainsLaunchBudgetBroadeningHint(const FString& ReasonLine); + + bool ContainsLaunchBudgetTighteningHint(const FString& ReasonLine); + + TArray DeduplicateCaseIds(const TArray& CaseIds); + + void ResolveCoachActionPlanPreferenceDefaults( + const FHyperTwistCoachBrief& CoachBrief, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + EHyperTwistTrainingDeliveryMode& InOutMode, + EHyperTwistTrainingSelectionPolicy& InOutSelectionPolicy + ); + + TArray ResolveCoachActionPlanFocusCaseIds( + const FHyperTwistCoachBrief& CoachBrief, + const TArray& LaunchedCaseIds, + int32 PlannedLaunchCaseCount + ); + + FHyperTwistTrainingCoachActionPlan NormalizeCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan + ); + + bool IsTerminalCoachQueueEntryState(EHyperTwistTrainingCoachSessionQueueEntryState EntryState); + + FString MakeCoachSessionQueueEntryFingerprint( + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry + ); + + FHyperTwistTrainingCoachSessionQueueState NormalizeCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& QueueState + ); + + bool DidCoachSessionQueueEntryChange( + const FHyperTwistTrainingCoachSessionQueueStateEntry& PreviousEntry, + const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry + ); + + FHyperTwistTrainingCoachSessionQueueHistoryEntry BuildCoachSessionQueueHistoryEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry, + const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry + ); } diff --git a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md index 75b297b..6fbda88 100644 --- a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md +++ b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md @@ -1633,3 +1633,49 @@ Latest native repository review-family split follow-up later on `2026-06-29`: repository library - the best next bounded seam remains the coach action-plan and coach-session-queue cluster rather than any fresh product-family widening + +Latest native repository coach-workflow split follow-up later on `2026-06-29`: + +- the next same-family native continuation kept the refactor lane disciplined + and narrowed the remaining training-repository hotspot without widening + simulator or website scope: + - moved the coach action-plan lifecycle plus coach session-queue state, + mutation, history, and persistence family out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - kept the broader `DeriveCoachSessionQueueExecutionSummary` method in the + main repository unit on purpose because it still depends on a larger helper + and reporting surface than this bounded extraction needed + - added the extracted implementation unit at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp` + - widened the existing private internal declaration seam at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h` + so the extracted workflow family can share only the bounded internal + helpers it actually needs + - reduced the main repository file to `31,089` lines while the extracted + coach-workflow unit now holds `892` lines +- the HyperTwist-owned structural loop stayed green on that exact native + packet: + - `scripts/run-hypertwist-sentrux-source-only.sh` + - `Quality: 6233` + - all `7` rules pass + - `scripts/run-hypertwist-sentrux-gate.sh` + - comparison result: + - `Quality: 6234 -> 6233` + - `Coupling: 0.15 -> 0.15` + - `Cycles: 0 -> 0` + - `God files: 1 -> 1` + - `No degradation detected` +- the canonical Windows Unreal proof for that native packet then passed + through the maintained reverse-SSH lane: + - reran `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'` + - `Target is up to date` + - `Result: Succeeded` + - UnrealBuildTool `Total execution time: 5.10 seconds` +- current truthful reading after this native follow-up: + - the coach-workflow extraction is now build-proven on the canonical Windows + lane and safe to land + - vanilla refactoring still is not fully complete as a code-shape task + because the training repository library still contains the remaining + same-family hotspot + - the next worthwhile continuation remains another bounded training-repository + seam rather than reopening product topology or renderer widening diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md index 7c2a8eb..1e85e37 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md @@ -2456,3 +2456,48 @@ Latest same-family responsive public-route expansion follow-up on `2026-06-29`: repository library still contains the remaining same-family hotspot - the best next bounded native seam remains the coach action-plan and coach-session-queue cluster instead of any fresh product-family widening + +## Latest native repository coach-workflow split follow-up later on `2026-06-29` + +- the next same-family native continuation then narrowed the remaining + training-repository hotspot again without widening simulator or website + scope: + - the coach action-plan lifecycle plus coach session-queue state, mutation, + history, and persistence family was moved out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - the broader `DeriveCoachSessionQueueExecutionSummary` method was kept in + the main repository unit on purpose because it still depends on a larger + helper and reporting surface than this bounded extraction needed + - the extracted implementation family now lives at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachWorkflow.cpp` + - the existing private helper declaration seam in + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h` + was widened so the workflow split can reuse only the bounded internal + helpers it actually needs + - the main repository file now stands at `31,089` lines and the extracted + coach-workflow unit stands at `892` lines +- the HyperTwist-owned structural loop stayed healthy on that exact native + state: + - `scripts/run-hypertwist-sentrux-source-only.sh` + - `Quality: 6233` + - all `7` rules pass + - `scripts/run-hypertwist-sentrux-gate.sh` + - comparison result: + - `Quality: 6234 -> 6233` + - `Coupling: 0.15 -> 0.15` + - `Cycles: 0 -> 0` + - `God files: 1 -> 1` + - `No degradation detected` +- the canonical Windows Unreal proof for that native split then passed through + the maintained reverse-SSH lane: + - `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'` + - `Target is up to date` + - `Result: Succeeded` + - UnrealBuildTool `Total execution time: 5.10 seconds` +- current truthful reading after this native follow-up: + - the coach-workflow extraction is now build-proven on the canonical Windows + lane + - vanilla refactoring still is not fully complete because the training + repository library still contains the remaining same-family hotspot + - the next worthwhile continuation remains another bounded training-repository + seam instead of fresh product-topology widening