Split HyperTwist coach workflow repository family

This commit is contained in:
axiomlogicnexus 2026-06-29 23:43:47 +00:00
parent c6a02c9147
commit c6a88aa9eb
5 changed files with 1027 additions and 887 deletions

View file

@ -23081,893 +23081,6 @@ TArray<FHyperTwistTrainingSessionTemplate> 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<FString>& 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<EHyperTwistCoachFocusLane>()->GetNameStringByValue(
static_cast<int64>(CoachBrief.FocusLane)
);
ActionPlan.PriorityLabel = StaticEnum<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(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<FHyperTwistTrainingCoachActionPlan> UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachActionPlan> 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<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(SummaryEntry.Priority)
);
QueueEntry.PriorityScore = SummaryEntry.PriorityScore;
QueueEntry.FocusLaneLabel = StaticEnum<EHyperTwistCoachFocusLane>()->GetNameStringByValue(
static_cast<int64>(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<FHyperTwistTrainingCoachSessionQueueState> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueState> 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<FHyperTwistTrainingCoachSessionQueueHistoryEntry> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> 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,

View file

@ -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<FString>& 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<EHyperTwistCoachFocusLane>()->GetNameStringByValue(
static_cast<int64>(CoachBrief.FocusLane)
);
ActionPlan.PriorityLabel = StaticEnum<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(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<FHyperTwistTrainingCoachActionPlan> UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachActionPlan> 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<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(SummaryEntry.Priority)
);
QueueEntry.PriorityScore = SummaryEntry.PriorityScore;
QueueEntry.FocusLaneLabel = StaticEnum<EHyperTwistCoachFocusLane>()->GetNameStringByValue(
static_cast<int64>(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<FHyperTwistTrainingCoachSessionQueueState> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueState> 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<FHyperTwistTrainingCoachSessionQueueHistoryEntry> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> 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;
}

View file

@ -23,4 +23,48 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
int32 RequestedMaxCases
);
bool ContainsLaunchBudgetBroadeningHint(const FString& ReasonLine);
bool ContainsLaunchBudgetTighteningHint(const FString& ReasonLine);
TArray<FString> DeduplicateCaseIds(const TArray<FString>& CaseIds);
void ResolveCoachActionPlanPreferenceDefaults(
const FHyperTwistCoachBrief& CoachBrief,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,
EHyperTwistTrainingDeliveryMode& InOutMode,
EHyperTwistTrainingSelectionPolicy& InOutSelectionPolicy
);
TArray<FString> ResolveCoachActionPlanFocusCaseIds(
const FHyperTwistCoachBrief& CoachBrief,
const TArray<FString>& 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
);
}

View file

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

View file

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