diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 6977212..4209eb0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -24559,624 +24559,6 @@ FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingReposito return Summary; } -FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachActionPlanSummary Summary; - if (UserId.IsEmpty()) - { - return Summary; - } - - Summary.UserId = UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); - - const TArray ActionPlans = ListCoachActionPlansForUser( - RepositoryState, - UserId - ); - if (ActionPlans.Num() == 0) - { - return Summary; - } - - Summary.TotalPlanCount = ActionPlans.Num(); - int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0}; - int32 HandoffKindCounts[4] = {0, 0, 0, 0}; - TMap FocusLaneCounts; - TMap ActionLabelCounts; - TMap HandoffSourceCounts; - int32 CompletedPlanDurationAccumulator = 0; - int32 CompletedPlanDurationCount = 0; - float PriorityScoreAccumulator = 0.0f; - FString LatestOpenPlanUtc; - FString LatestCompletedUtc; - - for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : ActionPlans) - { - PriorityScoreAccumulator += ActionPlan.PriorityScore; - Summary.TotalEntryCount += ActionPlan.Entries.Num(); - FocusLaneCounts.FindOrAdd(ActionPlan.FocusLaneLabel) += 1; - ActionLabelCounts.FindOrAdd(ActionPlan.PrimaryActionLabel) += 1; - - const int32 SuggestedModeIndex = static_cast(ActionPlan.SuggestedMode); - if (SuggestedModeIndex >= 0 && SuggestedModeIndex < UE_ARRAY_COUNT(SuggestedModeCounts)) - { - SuggestedModeCounts[SuggestedModeIndex] += 1; - } - const int32 HandoffKindIndex = static_cast(ActionPlan.PreferredCoachHandoffKind); - if (HandoffKindIndex >= 0 && HandoffKindIndex < UE_ARRAY_COUNT(HandoffKindCounts)) - { - HandoffKindCounts[HandoffKindIndex] += 1; - } - if (!ActionPlan.PreferredCoachHandoffSourceLabel.IsEmpty()) - { - HandoffSourceCounts.FindOrAdd(ActionPlan.PreferredCoachHandoffSourceLabel) += 1; - } - - int32 LocalCompletedEntries = 0; - for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries) - { - if (Entry.bCompleted) - { - LocalCompletedEntries += 1; - } - if (Entry.bPreferRecognitionReplayReview) - { - Summary.bHasRecognitionBiasedPlans = true; - } - } - - Summary.CompletedEntryCount += LocalCompletedEntries; - Summary.OpenEntryCount += ActionPlan.Entries.Num() - LocalCompletedEntries; - Summary.bHasRecognitionBiasedPlans = Summary.bHasRecognitionBiasedPlans || ActionPlan.bPreferRecognitionReplayReview; - - switch (ActionPlan.PlanState) - { - case EHyperTwistTrainingCoachActionPlanState::Draft: - Summary.DraftPlanCount += 1; - break; - case EHyperTwistTrainingCoachActionPlanState::Active: - Summary.ActivePlanCount += 1; - break; - case EHyperTwistTrainingCoachActionPlanState::Completed: - Summary.CompletedPlanCount += 1; - break; - case EHyperTwistTrainingCoachActionPlanState::Aborted: - Summary.AbortedPlanCount += 1; - break; - default: - break; - } - - if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Active - || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Draft) - { - const FString SortUtc = !ActionPlan.StartedAtUtc.IsEmpty() - ? ActionPlan.StartedAtUtc - : ActionPlan.CreatedAtUtc; - if (Summary.CurrentPlanId.IsEmpty() - || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestOpenPlanUtc) > 0) - { - Summary.CurrentPlanId = ActionPlan.PlanId; - Summary.FocusDeckId = ActionPlan.FocusDeckId; - Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; - LatestOpenPlanUtc = SortUtc; - } - } - - if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed - && !ActionPlan.CompletedAtUtc.IsEmpty()) - { - const int32 DurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( - ActionPlan.CreatedAtUtc, - ActionPlan.CompletedAtUtc - ); - if (DurationMs > 0) - { - CompletedPlanDurationAccumulator += DurationMs; - CompletedPlanDurationCount += 1; - if (Summary.BestCompletedPlanDurationMs == 0 || DurationMs < Summary.BestCompletedPlanDurationMs) - { - Summary.BestCompletedPlanDurationMs = DurationMs; - } - } - - if (Summary.LastCompletedPlanId.IsEmpty() - || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ActionPlan.CompletedAtUtc, LatestCompletedUtc) > 0) - { - Summary.LastCompletedPlanId = ActionPlan.PlanId; - LatestCompletedUtc = ActionPlan.CompletedAtUtc; - if (Summary.CurrentPlanId.IsEmpty()) - { - Summary.FocusDeckId = ActionPlan.FocusDeckId; - Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; - } - } - } - - if (ActionPlan.bNeedsCoachReview - || ActionPlan.bNeedsCadenceRecovery - || ActionPlan.bNeedsVarietyRotation - || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted) - { - Summary.bNeedsCoachIntervention = true; - } - } - - Summary.AveragePriorityScore = PriorityScoreAccumulator / static_cast(Summary.TotalPlanCount); - if (CompletedPlanDurationCount > 0) - { - Summary.AverageCompletedPlanDurationMs = - CompletedPlanDurationAccumulator / CompletedPlanDurationCount; - } - - int32 BestModeCount = 0; - for (int32 Index = 0; Index < UE_ARRAY_COUNT(SuggestedModeCounts); ++Index) - { - if (SuggestedModeCounts[Index] > BestModeCount) - { - BestModeCount = SuggestedModeCounts[Index]; - Summary.DominantSuggestedMode = static_cast(Index); - } - } - - int32 BestHandoffKindCount = 0; - for (int32 Index = 0; Index < UE_ARRAY_COUNT(HandoffKindCounts); ++Index) - { - if (HandoffKindCounts[Index] > BestHandoffKindCount) - { - BestHandoffKindCount = HandoffKindCounts[Index]; - Summary.DominantPreferredCoachHandoffKind = - static_cast(Index); - } - } - - int32 BestFocusLaneCount = 0; - for (const TPair& Pair : FocusLaneCounts) - { - if (Pair.Value > BestFocusLaneCount) - { - BestFocusLaneCount = Pair.Value; - Summary.DominantFocusLaneLabel = Pair.Key; - } - } - - int32 BestActionLabelCount = 0; - for (const TPair& Pair : ActionLabelCounts) - { - if (Pair.Value > BestActionLabelCount) - { - BestActionLabelCount = Pair.Value; - Summary.DominantActionLabel = Pair.Key; - } - } - - int32 BestHandoffSourceCount = 0; - for (const TPair& Pair : HandoffSourceCounts) - { - if (Pair.Value > BestHandoffSourceCount) - { - BestHandoffSourceCount = Pair.Value; - Summary.DominantPreferredCoachHandoffSourceLabel = Pair.Key; - } - } - - Summary.ActionPressureScore = - static_cast(Summary.ActivePlanCount * 3) - + static_cast(Summary.DraftPlanCount * 2) - + static_cast(Summary.AbortedPlanCount * 2) - + (static_cast(Summary.OpenEntryCount) * 0.5f) - + (Summary.bNeedsCoachIntervention ? 5.0f : 0.0f); - - return Summary; -} - -FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachActionPlan& ActionPlan, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachActionPlanOutcomeSummary Summary; - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); - if (!NormalizedPlan.IsStructurallyValid()) - { - return Summary; - } - - Summary.UserId = NormalizedPlan.UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - !ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedPlan.ReferenceUtc - ); - Summary.PlanId = NormalizedPlan.PlanId; - Summary.SourceSessionId = NormalizedPlan.SourceSessionId; - Summary.FocusDeckId = NormalizedPlan.FocusDeckId; - Summary.FocusMethodSegmentId = NormalizedPlan.FocusMethodSegmentId; - Summary.PrimaryActionLabel = NormalizedPlan.PrimaryActionLabel; - Summary.PlanState = NormalizedPlan.PlanState; - Summary.SuggestedMode = NormalizedPlan.SuggestedMode; - Summary.PreferredCoachHandoffKind = NormalizedPlan.PreferredCoachHandoffKind; - Summary.PreferredCoachHandoffSourceLabel = NormalizedPlan.PreferredCoachHandoffSourceLabel; - Summary.RequestedLaunchCaseCount = NormalizedPlan.RequestedLaunchCaseCount; - Summary.PlannedLaunchCaseCount = NormalizedPlan.PlannedLaunchCaseCount; - Summary.bBroadenedLaunchBudget = NormalizedPlan.bBroadenedLaunchBudget; - Summary.bTightenedLaunchBudget = NormalizedPlan.bTightenedLaunchBudget; - Summary.LaunchBudgetReasonLine = NormalizedPlan.LaunchBudgetReasonLine; - Summary.DeferredLaunchCaseIds = NormalizedPlan.DeferredLaunchCaseIds; - - TSet FocusCaseIds; - for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) - { - if (!FocusCaseId.IsEmpty()) - { - FocusCaseIds.Add(FocusCaseId); - } - } - - TSet CompletedCaseIds; - for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) - { - if (!Entry.CaseId.IsEmpty() && FocusCaseIds.Num() == 0) - { - FocusCaseIds.Add(Entry.CaseId); - } - - if (Entry.bCompleted && !Entry.CaseId.IsEmpty()) - { - CompletedCaseIds.Add(Entry.CaseId); - } - } - - Summary.FocusCaseCount = FocusCaseIds.Num(); - - FHyperTwistTrainingRunRecord RunRecord; - if (!NormalizedPlan.SourceSessionId.IsEmpty() - && TryGetRunRecord(RepositoryState, NormalizedPlan.SourceSessionId, RunRecord)) - { - TSet TouchedCaseIds; - int32 TotalTimeAccumulator = 0; - int32 TotalTimeSampleCount = 0; - int32 RecognitionTimeAccumulator = 0; - int32 RecognitionTimeSampleCount = 0; - - for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory) - { - if (FocusCaseIds.Num() > 0 && !FocusCaseIds.Contains(AttemptHistoryEntry.CaseId)) - { - continue; - } - - Summary.AttemptCount += 1; - TouchedCaseIds.Add(AttemptHistoryEntry.CaseId); - - if (!AttemptHistoryEntry.ReplayId.IsEmpty()) - { - Summary.ReplayBackedAttemptCount += 1; - } - - if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted - || AttemptHistoryEntry.RecognitionTimeMs > 0) - { - Summary.RecognitionAttemptCount += 1; - } - - if (AttemptHistoryEntry.TotalTimeMs > 0) - { - TotalTimeAccumulator += AttemptHistoryEntry.TotalTimeMs; - TotalTimeSampleCount += 1; - if (Summary.BestTotalTimeMs == 0 || AttemptHistoryEntry.TotalTimeMs < Summary.BestTotalTimeMs) - { - Summary.BestTotalTimeMs = AttemptHistoryEntry.TotalTimeMs; - } - } - - if (AttemptHistoryEntry.RecognitionTimeMs > 0) - { - RecognitionTimeAccumulator += AttemptHistoryEntry.RecognitionTimeMs; - RecognitionTimeSampleCount += 1; - } - - switch (AttemptHistoryEntry.Result) - { - case EHyperTwistTrainingAttemptResult::Success: - Summary.SuccessCount += 1; - if (!AttemptHistoryEntry.CaseId.IsEmpty()) - { - CompletedCaseIds.Add(AttemptHistoryEntry.CaseId); - } - break; - - case EHyperTwistTrainingAttemptResult::Failure: - Summary.FailureCount += 1; - break; - - case EHyperTwistTrainingAttemptResult::Timeout: - Summary.TimeoutCount += 1; - break; - - case EHyperTwistTrainingAttemptResult::DNF: - Summary.DNFCount += 1; - break; - - case EHyperTwistTrainingAttemptResult::Aborted: - Summary.AbortedCount += 1; - break; - - default: - break; - } - } - - Summary.TouchedFocusCaseCount = TouchedCaseIds.Num(); - if (TotalTimeSampleCount > 0) - { - Summary.AverageTotalTimeMs = TotalTimeAccumulator / TotalTimeSampleCount; - } - if (RecognitionTimeSampleCount > 0) - { - Summary.AverageRecognitionTimeMs = RecognitionTimeAccumulator / RecognitionTimeSampleCount; - } - } - - Summary.CompletedFocusCaseCount = CompletedCaseIds.Num(); - Summary.UntouchedFocusCaseCount = FMath::Max(0, Summary.FocusCaseCount - Summary.TouchedFocusCaseCount); - for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) - { - if (!FocusCaseId.IsEmpty() && !CompletedCaseIds.Contains(FocusCaseId)) - { - Summary.FollowUpCaseIds.Add(FocusCaseId); - } - } - if (Summary.FollowUpCaseIds.Num() == 0) - { - for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) - { - if (!Entry.CaseId.IsEmpty() - && !CompletedCaseIds.Contains(Entry.CaseId) - && !Summary.FollowUpCaseIds.Contains(Entry.CaseId)) - { - Summary.FollowUpCaseIds.Add(Entry.CaseId); - } - } - } - if (Summary.FocusCaseCount > 0) - { - Summary.CompletionRate = static_cast(Summary.CompletedFocusCaseCount) - / static_cast(Summary.FocusCaseCount); - } - - const FString PlanStartUtc = !NormalizedPlan.StartedAtUtc.IsEmpty() - ? NormalizedPlan.StartedAtUtc - : NormalizedPlan.CreatedAtUtc; - const FString PlanEndUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() - ? NormalizedPlan.CompletedAtUtc - : Summary.ReferenceUtc; - if (!PlanStartUtc.IsEmpty() && !PlanEndUtc.IsEmpty()) - { - Summary.PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( - PlanStartUtc, - PlanEndUtc - ); - } - - const bool bPlanTerminal = - NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed - || NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted; - Summary.bMetPrimaryGoal = - bPlanTerminal - && NormalizedPlan.PlanState != EHyperTwistTrainingCoachActionPlanState::Aborted - && (Summary.FocusCaseCount == 0 - ? Summary.SuccessCount > 0 - : Summary.CompletedFocusCaseCount >= Summary.FocusCaseCount); - - Summary.OutcomeScore = - (Summary.CompletionRate * 100.0f) - + (static_cast(Summary.SuccessCount) * 4.0f) - - (static_cast(Summary.FailureCount) * 2.0f) - - (static_cast(Summary.TimeoutCount) * 3.0f) - - (static_cast(Summary.DNFCount) * 4.0f) - - (static_cast(Summary.AbortedCount) * 5.0f) - - (static_cast(Summary.UntouchedFocusCaseCount) * 4.0f) - + (static_cast(Summary.ReplayBackedAttemptCount) * 0.5f); - - Summary.bNeedsFollowUpPlan = - Summary.FollowUpCaseIds.Num() > 0 - || Summary.DeferredLaunchCaseIds.Num() > 0 - || ( - bPlanTerminal - && ( - NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted - || !Summary.bMetPrimaryGoal - || Summary.FailureCount > 0 - || Summary.TimeoutCount > 0 - || Summary.DNFCount > 0 - || (NormalizedPlan.bNeedsCoachReview && Summary.ReplayBackedAttemptCount == 0) - )); - - return Summary; -} - -FHyperTwistTrainingCoachActionSequenceSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachActionPlan& AnchorPlan, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachActionSequenceSummary Summary; - const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); - if (!NormalizedAnchorPlan.IsStructurallyValid()) - { - return Summary; - } - - const FString RootPlanId = !NormalizedAnchorPlan.RootPlanId.IsEmpty() - ? NormalizedAnchorPlan.RootPlanId - : NormalizedAnchorPlan.PlanId; - Summary.UserId = NormalizedAnchorPlan.UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( - !ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedAnchorPlan.ReferenceUtc - ); - Summary.RootPlanId = RootPlanId; - - const TArray UserPlans = ListCoachActionPlansForUser( - RepositoryState, - NormalizedAnchorPlan.UserId - ); - TArray SequencePlans; - for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) - { - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); - const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() - ? NormalizedPlan.RootPlanId - : NormalizedPlan.PlanId; - if (CandidateRootPlanId == RootPlanId - || NormalizedPlan.PlanId == RootPlanId - || NormalizedPlan.ParentPlanId == RootPlanId) - { - SequencePlans.Add(NormalizedPlan); - } - } - if (SequencePlans.Num() == 0) - { - SequencePlans.Add(NormalizedAnchorPlan); - } - const auto ResolvePlanSortUtc = [](const FHyperTwistTrainingCoachActionPlan& Plan) -> FString - { - return !Plan.CompletedAtUtc.IsEmpty() - ? Plan.CompletedAtUtc - : (!Plan.StartedAtUtc.IsEmpty() ? Plan.StartedAtUtc : Plan.CreatedAtUtc); - }; - SequencePlans.Sort( - [&ResolvePlanSortUtc](const FHyperTwistTrainingCoachActionPlan& Left, - const FHyperTwistTrainingCoachActionPlan& Right) - { - const FString LeftSortUtc = ResolvePlanSortUtc(Left); - const FString RightSortUtc = ResolvePlanSortUtc(Right); - const int32 CompareResult = - HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(LeftSortUtc, RightSortUtc); - if (CompareResult != 0) - { - return CompareResult < 0; - } - if (Left.FollowUpDepth != Right.FollowUpDepth) - { - return Left.FollowUpDepth < Right.FollowUpDepth; - } - - return Left.PlanId < Right.PlanId; - } - ); - - FString LatestSortUtc; - TSet OutstandingFollowUpCases; - float CompletionRateAccumulator = 0.0f; - float OutcomeScoreAccumulator = 0.0f; - bool bHasGenericFollowUpPressure = false; - - for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans) - { - Summary.PlanCount += 1; - Summary.CarryForwardPlanCount += Plan.FollowUpDepth > 0 ? 1 : 0; - Summary.MaxFollowUpDepth = FMath::Max(Summary.MaxFollowUpDepth, Plan.FollowUpDepth); - - switch (Plan.PlanState) - { - case EHyperTwistTrainingCoachActionPlanState::Active: - Summary.ActivePlanCount += 1; - Summary.bHasOpenPlan = true; - break; - case EHyperTwistTrainingCoachActionPlanState::Completed: - Summary.CompletedPlanCount += 1; - break; - case EHyperTwistTrainingCoachActionPlanState::Aborted: - Summary.AbortedPlanCount += 1; - break; - default: - Summary.bHasOpenPlan = true; - break; - } - - const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary = - DeriveCoachActionPlanOutcomeSummary(RepositoryState, Plan, Summary.ReferenceUtc); - Summary.TotalFocusCaseCount += - OutcomeSummary.FocusCaseCount + OutcomeSummary.DeferredLaunchCaseIds.Num(); - CompletionRateAccumulator += OutcomeSummary.CompletionRate; - OutcomeScoreAccumulator += OutcomeSummary.OutcomeScore; - TSet PlanFocusCaseIds; - for (const FString& FocusCaseId : Plan.FocusCaseIds) - { - if (!FocusCaseId.IsEmpty()) - { - PlanFocusCaseIds.Add(FocusCaseId); - } - } - for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries) - { - if (!Entry.CaseId.IsEmpty()) - { - PlanFocusCaseIds.Add(Entry.CaseId); - } - } - TSet PlanOutstandingCaseIds; - for (const FString& CaseId : OutcomeSummary.FollowUpCaseIds) - { - if (!CaseId.IsEmpty()) - { - PlanOutstandingCaseIds.Add(CaseId); - OutstandingFollowUpCases.Add(CaseId); - } - } - for (const FString& CaseId : OutcomeSummary.DeferredLaunchCaseIds) - { - if (!CaseId.IsEmpty()) - { - PlanOutstandingCaseIds.Add(CaseId); - OutstandingFollowUpCases.Add(CaseId); - } - } - for (const FString& FocusCaseId : PlanFocusCaseIds) - { - if (!PlanOutstandingCaseIds.Contains(FocusCaseId)) - { - OutstandingFollowUpCases.Remove(FocusCaseId); - } - } - bHasGenericFollowUpPressure = - OutcomeSummary.bNeedsFollowUpPlan && PlanOutstandingCaseIds.Num() == 0; - - const FString SortUtc = ResolvePlanSortUtc(Plan); - if (Summary.LatestPlanId.IsEmpty() - || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) > 0 - || (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) == 0 - && Plan.FollowUpDepth >= Summary.MaxFollowUpDepth)) - { - Summary.LatestPlanId = Plan.PlanId; - Summary.FocusDeckId = Plan.FocusDeckId; - Summary.FocusMethodSegmentId = Plan.FocusMethodSegmentId; - Summary.LatestOutcomeScore = OutcomeSummary.OutcomeScore; - LatestSortUtc = SortUtc; - } - } - - if (Summary.PlanCount > 0) - { - Summary.AverageCompletionRate = CompletionRateAccumulator / static_cast(Summary.PlanCount); - Summary.AverageOutcomeScore = OutcomeScoreAccumulator / static_cast(Summary.PlanCount); - } - Summary.UnresolvedFollowUpCaseCount = OutstandingFollowUpCases.Num(); - Summary.bNeedsMoreFollowUp = - Summary.UnresolvedFollowUpCaseCount > 0 || bHasGenericFollowUpPressure || Summary.bHasOpenPlan; - - return Summary; -} - namespace HyperTwistTrainingRepositoryLibraryInternal { FHyperTwistTrainingCoachActionClosureSummary DeriveCoachActionClosureSummaryWithCoachMemory( diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp new file mode 100644 index 0000000..3a2bb5d --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp @@ -0,0 +1,625 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" + +#include "HyperTwistTrainingRepositoryLibraryInternal.h" + +FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionPlanSummary Summary; + if (UserId.IsEmpty()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + + const TArray ActionPlans = ListCoachActionPlansForUser( + RepositoryState, + UserId + ); + if (ActionPlans.Num() == 0) + { + return Summary; + } + + Summary.TotalPlanCount = ActionPlans.Num(); + int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0}; + int32 HandoffKindCounts[4] = {0, 0, 0, 0}; + TMap FocusLaneCounts; + TMap ActionLabelCounts; + TMap HandoffSourceCounts; + int32 CompletedPlanDurationAccumulator = 0; + int32 CompletedPlanDurationCount = 0; + float PriorityScoreAccumulator = 0.0f; + FString LatestOpenPlanUtc; + FString LatestCompletedUtc; + + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : ActionPlans) + { + PriorityScoreAccumulator += ActionPlan.PriorityScore; + Summary.TotalEntryCount += ActionPlan.Entries.Num(); + FocusLaneCounts.FindOrAdd(ActionPlan.FocusLaneLabel) += 1; + ActionLabelCounts.FindOrAdd(ActionPlan.PrimaryActionLabel) += 1; + + const int32 SuggestedModeIndex = static_cast(ActionPlan.SuggestedMode); + if (SuggestedModeIndex >= 0 && SuggestedModeIndex < UE_ARRAY_COUNT(SuggestedModeCounts)) + { + SuggestedModeCounts[SuggestedModeIndex] += 1; + } + const int32 HandoffKindIndex = static_cast(ActionPlan.PreferredCoachHandoffKind); + if (HandoffKindIndex >= 0 && HandoffKindIndex < UE_ARRAY_COUNT(HandoffKindCounts)) + { + HandoffKindCounts[HandoffKindIndex] += 1; + } + if (!ActionPlan.PreferredCoachHandoffSourceLabel.IsEmpty()) + { + HandoffSourceCounts.FindOrAdd(ActionPlan.PreferredCoachHandoffSourceLabel) += 1; + } + + int32 LocalCompletedEntries = 0; + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries) + { + if (Entry.bCompleted) + { + LocalCompletedEntries += 1; + } + if (Entry.bPreferRecognitionReplayReview) + { + Summary.bHasRecognitionBiasedPlans = true; + } + } + + Summary.CompletedEntryCount += LocalCompletedEntries; + Summary.OpenEntryCount += ActionPlan.Entries.Num() - LocalCompletedEntries; + Summary.bHasRecognitionBiasedPlans = + Summary.bHasRecognitionBiasedPlans || ActionPlan.bPreferRecognitionReplayReview; + + switch (ActionPlan.PlanState) + { + case EHyperTwistTrainingCoachActionPlanState::Draft: + Summary.DraftPlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Active: + Summary.ActivePlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Completed: + Summary.CompletedPlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Aborted: + Summary.AbortedPlanCount += 1; + break; + default: + break; + } + + if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Active + || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Draft) + { + const FString SortUtc = !ActionPlan.StartedAtUtc.IsEmpty() + ? ActionPlan.StartedAtUtc + : ActionPlan.CreatedAtUtc; + if (Summary.CurrentPlanId.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestOpenPlanUtc) > 0) + { + Summary.CurrentPlanId = ActionPlan.PlanId; + Summary.FocusDeckId = ActionPlan.FocusDeckId; + Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; + LatestOpenPlanUtc = SortUtc; + } + } + + if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed + && !ActionPlan.CompletedAtUtc.IsEmpty()) + { + const int32 DurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( + ActionPlan.CreatedAtUtc, + ActionPlan.CompletedAtUtc + ); + if (DurationMs > 0) + { + CompletedPlanDurationAccumulator += DurationMs; + CompletedPlanDurationCount += 1; + if (Summary.BestCompletedPlanDurationMs == 0 || DurationMs < Summary.BestCompletedPlanDurationMs) + { + Summary.BestCompletedPlanDurationMs = DurationMs; + } + } + + if (Summary.LastCompletedPlanId.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ActionPlan.CompletedAtUtc, LatestCompletedUtc) > 0) + { + Summary.LastCompletedPlanId = ActionPlan.PlanId; + LatestCompletedUtc = ActionPlan.CompletedAtUtc; + if (Summary.CurrentPlanId.IsEmpty()) + { + Summary.FocusDeckId = ActionPlan.FocusDeckId; + Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; + } + } + } + + if (ActionPlan.bNeedsCoachReview + || ActionPlan.bNeedsCadenceRecovery + || ActionPlan.bNeedsVarietyRotation + || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted) + { + Summary.bNeedsCoachIntervention = true; + } + } + + Summary.AveragePriorityScore = PriorityScoreAccumulator / static_cast(Summary.TotalPlanCount); + if (CompletedPlanDurationCount > 0) + { + Summary.AverageCompletedPlanDurationMs = + CompletedPlanDurationAccumulator / CompletedPlanDurationCount; + } + + int32 BestModeCount = 0; + for (int32 Index = 0; Index < UE_ARRAY_COUNT(SuggestedModeCounts); ++Index) + { + if (SuggestedModeCounts[Index] > BestModeCount) + { + BestModeCount = SuggestedModeCounts[Index]; + Summary.DominantSuggestedMode = static_cast(Index); + } + } + + int32 BestHandoffKindCount = 0; + for (int32 Index = 0; Index < UE_ARRAY_COUNT(HandoffKindCounts); ++Index) + { + if (HandoffKindCounts[Index] > BestHandoffKindCount) + { + BestHandoffKindCount = HandoffKindCounts[Index]; + Summary.DominantPreferredCoachHandoffKind = + static_cast(Index); + } + } + + int32 BestFocusLaneCount = 0; + for (const TPair& Pair : FocusLaneCounts) + { + if (Pair.Value > BestFocusLaneCount) + { + BestFocusLaneCount = Pair.Value; + Summary.DominantFocusLaneLabel = Pair.Key; + } + } + + int32 BestActionLabelCount = 0; + for (const TPair& Pair : ActionLabelCounts) + { + if (Pair.Value > BestActionLabelCount) + { + BestActionLabelCount = Pair.Value; + Summary.DominantActionLabel = Pair.Key; + } + } + + int32 BestHandoffSourceCount = 0; + for (const TPair& Pair : HandoffSourceCounts) + { + if (Pair.Value > BestHandoffSourceCount) + { + BestHandoffSourceCount = Pair.Value; + Summary.DominantPreferredCoachHandoffSourceLabel = Pair.Key; + } + } + + Summary.ActionPressureScore = + static_cast(Summary.ActivePlanCount * 3) + + static_cast(Summary.DraftPlanCount * 2) + + static_cast(Summary.AbortedPlanCount * 2) + + (static_cast(Summary.OpenEntryCount) * 0.5f) + + (Summary.bNeedsCoachIntervention ? 5.0f : 0.0f); + + return Summary; +} + +FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionPlanOutcomeSummary Summary; + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!NormalizedPlan.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = NormalizedPlan.UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + !ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedPlan.ReferenceUtc + ); + Summary.PlanId = NormalizedPlan.PlanId; + Summary.SourceSessionId = NormalizedPlan.SourceSessionId; + Summary.FocusDeckId = NormalizedPlan.FocusDeckId; + Summary.FocusMethodSegmentId = NormalizedPlan.FocusMethodSegmentId; + Summary.PrimaryActionLabel = NormalizedPlan.PrimaryActionLabel; + Summary.PlanState = NormalizedPlan.PlanState; + Summary.SuggestedMode = NormalizedPlan.SuggestedMode; + Summary.PreferredCoachHandoffKind = NormalizedPlan.PreferredCoachHandoffKind; + Summary.PreferredCoachHandoffSourceLabel = NormalizedPlan.PreferredCoachHandoffSourceLabel; + Summary.RequestedLaunchCaseCount = NormalizedPlan.RequestedLaunchCaseCount; + Summary.PlannedLaunchCaseCount = NormalizedPlan.PlannedLaunchCaseCount; + Summary.bBroadenedLaunchBudget = NormalizedPlan.bBroadenedLaunchBudget; + Summary.bTightenedLaunchBudget = NormalizedPlan.bTightenedLaunchBudget; + Summary.LaunchBudgetReasonLine = NormalizedPlan.LaunchBudgetReasonLine; + Summary.DeferredLaunchCaseIds = NormalizedPlan.DeferredLaunchCaseIds; + + TSet FocusCaseIds; + for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty()) + { + FocusCaseIds.Add(FocusCaseId); + } + } + + TSet CompletedCaseIds; + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) + { + if (!Entry.CaseId.IsEmpty() && FocusCaseIds.Num() == 0) + { + FocusCaseIds.Add(Entry.CaseId); + } + + if (Entry.bCompleted && !Entry.CaseId.IsEmpty()) + { + CompletedCaseIds.Add(Entry.CaseId); + } + } + + Summary.FocusCaseCount = FocusCaseIds.Num(); + + FHyperTwistTrainingRunRecord RunRecord; + if (!NormalizedPlan.SourceSessionId.IsEmpty() + && TryGetRunRecord(RepositoryState, NormalizedPlan.SourceSessionId, RunRecord)) + { + TSet TouchedCaseIds; + int32 TotalTimeAccumulator = 0; + int32 TotalTimeSampleCount = 0; + int32 RecognitionTimeAccumulator = 0; + int32 RecognitionTimeSampleCount = 0; + + for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory) + { + if (FocusCaseIds.Num() > 0 && !FocusCaseIds.Contains(AttemptHistoryEntry.CaseId)) + { + continue; + } + + Summary.AttemptCount += 1; + TouchedCaseIds.Add(AttemptHistoryEntry.CaseId); + + if (!AttemptHistoryEntry.ReplayId.IsEmpty()) + { + Summary.ReplayBackedAttemptCount += 1; + } + + if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted + || AttemptHistoryEntry.RecognitionTimeMs > 0) + { + Summary.RecognitionAttemptCount += 1; + } + + if (AttemptHistoryEntry.TotalTimeMs > 0) + { + TotalTimeAccumulator += AttemptHistoryEntry.TotalTimeMs; + TotalTimeSampleCount += 1; + if (Summary.BestTotalTimeMs == 0 || AttemptHistoryEntry.TotalTimeMs < Summary.BestTotalTimeMs) + { + Summary.BestTotalTimeMs = AttemptHistoryEntry.TotalTimeMs; + } + } + + if (AttemptHistoryEntry.RecognitionTimeMs > 0) + { + RecognitionTimeAccumulator += AttemptHistoryEntry.RecognitionTimeMs; + RecognitionTimeSampleCount += 1; + } + + switch (AttemptHistoryEntry.Result) + { + case EHyperTwistTrainingAttemptResult::Success: + Summary.SuccessCount += 1; + if (!AttemptHistoryEntry.CaseId.IsEmpty()) + { + CompletedCaseIds.Add(AttemptHistoryEntry.CaseId); + } + break; + + case EHyperTwistTrainingAttemptResult::Failure: + Summary.FailureCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::Timeout: + Summary.TimeoutCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::DNF: + Summary.DNFCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::Aborted: + Summary.AbortedCount += 1; + break; + + default: + break; + } + } + + Summary.TouchedFocusCaseCount = TouchedCaseIds.Num(); + if (TotalTimeSampleCount > 0) + { + Summary.AverageTotalTimeMs = TotalTimeAccumulator / TotalTimeSampleCount; + } + if (RecognitionTimeSampleCount > 0) + { + Summary.AverageRecognitionTimeMs = RecognitionTimeAccumulator / RecognitionTimeSampleCount; + } + } + + Summary.CompletedFocusCaseCount = CompletedCaseIds.Num(); + Summary.UntouchedFocusCaseCount = FMath::Max(0, Summary.FocusCaseCount - Summary.TouchedFocusCaseCount); + for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty() && !CompletedCaseIds.Contains(FocusCaseId)) + { + Summary.FollowUpCaseIds.Add(FocusCaseId); + } + } + if (Summary.FollowUpCaseIds.Num() == 0) + { + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) + { + if (!Entry.CaseId.IsEmpty() + && !CompletedCaseIds.Contains(Entry.CaseId) + && !Summary.FollowUpCaseIds.Contains(Entry.CaseId)) + { + Summary.FollowUpCaseIds.Add(Entry.CaseId); + } + } + } + if (Summary.FocusCaseCount > 0) + { + Summary.CompletionRate = static_cast(Summary.CompletedFocusCaseCount) + / static_cast(Summary.FocusCaseCount); + } + + const FString PlanStartUtc = !NormalizedPlan.StartedAtUtc.IsEmpty() + ? NormalizedPlan.StartedAtUtc + : NormalizedPlan.CreatedAtUtc; + const FString PlanEndUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() + ? NormalizedPlan.CompletedAtUtc + : Summary.ReferenceUtc; + if (!PlanStartUtc.IsEmpty() && !PlanEndUtc.IsEmpty()) + { + Summary.PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( + PlanStartUtc, + PlanEndUtc + ); + } + + const bool bPlanTerminal = + NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed + || NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted; + Summary.bMetPrimaryGoal = + bPlanTerminal + && NormalizedPlan.PlanState != EHyperTwistTrainingCoachActionPlanState::Aborted + && (Summary.FocusCaseCount == 0 + ? Summary.SuccessCount > 0 + : Summary.CompletedFocusCaseCount >= Summary.FocusCaseCount); + + Summary.OutcomeScore = + (Summary.CompletionRate * 100.0f) + + (static_cast(Summary.SuccessCount) * 4.0f) + - (static_cast(Summary.FailureCount) * 2.0f) + - (static_cast(Summary.TimeoutCount) * 3.0f) + - (static_cast(Summary.DNFCount) * 4.0f) + - (static_cast(Summary.AbortedCount) * 5.0f) + - (static_cast(Summary.UntouchedFocusCaseCount) * 4.0f) + + (static_cast(Summary.ReplayBackedAttemptCount) * 0.5f); + + Summary.bNeedsFollowUpPlan = + Summary.FollowUpCaseIds.Num() > 0 + || Summary.DeferredLaunchCaseIds.Num() > 0 + || ( + bPlanTerminal + && ( + NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted + || !Summary.bMetPrimaryGoal + || Summary.FailureCount > 0 + || Summary.TimeoutCount > 0 + || Summary.DNFCount > 0 + || (NormalizedPlan.bNeedsCoachReview && Summary.ReplayBackedAttemptCount == 0) + ) + ); + + return Summary; +} + +FHyperTwistTrainingCoachActionSequenceSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& AnchorPlan, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionSequenceSummary Summary; + const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); + if (!NormalizedAnchorPlan.IsStructurallyValid()) + { + return Summary; + } + + const FString RootPlanId = !NormalizedAnchorPlan.RootPlanId.IsEmpty() + ? NormalizedAnchorPlan.RootPlanId + : NormalizedAnchorPlan.PlanId; + Summary.UserId = NormalizedAnchorPlan.UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + !ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedAnchorPlan.ReferenceUtc + ); + Summary.RootPlanId = RootPlanId; + + const TArray UserPlans = ListCoachActionPlansForUser( + RepositoryState, + NormalizedAnchorPlan.UserId + ); + TArray SequencePlans; + for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) + { + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); + const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() + ? NormalizedPlan.RootPlanId + : NormalizedPlan.PlanId; + if (CandidateRootPlanId == RootPlanId + || NormalizedPlan.PlanId == RootPlanId + || NormalizedPlan.ParentPlanId == RootPlanId) + { + SequencePlans.Add(NormalizedPlan); + } + } + if (SequencePlans.Num() == 0) + { + SequencePlans.Add(NormalizedAnchorPlan); + } + const auto ResolvePlanSortUtc = [](const FHyperTwistTrainingCoachActionPlan& Plan) -> FString + { + return !Plan.CompletedAtUtc.IsEmpty() + ? Plan.CompletedAtUtc + : (!Plan.StartedAtUtc.IsEmpty() ? Plan.StartedAtUtc : Plan.CreatedAtUtc); + }; + SequencePlans.Sort( + [&ResolvePlanSortUtc](const FHyperTwistTrainingCoachActionPlan& Left, + const FHyperTwistTrainingCoachActionPlan& Right) + { + const FString LeftSortUtc = ResolvePlanSortUtc(Left); + const FString RightSortUtc = ResolvePlanSortUtc(Right); + const int32 CompareResult = + HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(LeftSortUtc, RightSortUtc); + if (CompareResult != 0) + { + return CompareResult < 0; + } + if (Left.FollowUpDepth != Right.FollowUpDepth) + { + return Left.FollowUpDepth < Right.FollowUpDepth; + } + + return Left.PlanId < Right.PlanId; + } + ); + + FString LatestSortUtc; + TSet OutstandingFollowUpCases; + float CompletionRateAccumulator = 0.0f; + float OutcomeScoreAccumulator = 0.0f; + bool bHasGenericFollowUpPressure = false; + + for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans) + { + Summary.PlanCount += 1; + Summary.CarryForwardPlanCount += Plan.FollowUpDepth > 0 ? 1 : 0; + Summary.MaxFollowUpDepth = FMath::Max(Summary.MaxFollowUpDepth, Plan.FollowUpDepth); + + switch (Plan.PlanState) + { + case EHyperTwistTrainingCoachActionPlanState::Active: + Summary.ActivePlanCount += 1; + Summary.bHasOpenPlan = true; + break; + case EHyperTwistTrainingCoachActionPlanState::Completed: + Summary.CompletedPlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Aborted: + Summary.AbortedPlanCount += 1; + break; + default: + Summary.bHasOpenPlan = true; + break; + } + + const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary = + DeriveCoachActionPlanOutcomeSummary(RepositoryState, Plan, Summary.ReferenceUtc); + Summary.TotalFocusCaseCount += + OutcomeSummary.FocusCaseCount + OutcomeSummary.DeferredLaunchCaseIds.Num(); + CompletionRateAccumulator += OutcomeSummary.CompletionRate; + OutcomeScoreAccumulator += OutcomeSummary.OutcomeScore; + TSet PlanFocusCaseIds; + for (const FString& FocusCaseId : Plan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty()) + { + PlanFocusCaseIds.Add(FocusCaseId); + } + } + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries) + { + if (!Entry.CaseId.IsEmpty()) + { + PlanFocusCaseIds.Add(Entry.CaseId); + } + } + TSet PlanOutstandingCaseIds; + for (const FString& CaseId : OutcomeSummary.FollowUpCaseIds) + { + if (!CaseId.IsEmpty()) + { + PlanOutstandingCaseIds.Add(CaseId); + OutstandingFollowUpCases.Add(CaseId); + } + } + for (const FString& CaseId : OutcomeSummary.DeferredLaunchCaseIds) + { + if (!CaseId.IsEmpty()) + { + PlanOutstandingCaseIds.Add(CaseId); + OutstandingFollowUpCases.Add(CaseId); + } + } + for (const FString& FocusCaseId : PlanFocusCaseIds) + { + if (!PlanOutstandingCaseIds.Contains(FocusCaseId)) + { + OutstandingFollowUpCases.Remove(FocusCaseId); + } + } + bHasGenericFollowUpPressure = + OutcomeSummary.bNeedsFollowUpPlan && PlanOutstandingCaseIds.Num() == 0; + + const FString SortUtc = ResolvePlanSortUtc(Plan); + if (Summary.LatestPlanId.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) > 0 + || (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) == 0 + && Plan.FollowUpDepth >= Summary.MaxFollowUpDepth)) + { + Summary.LatestPlanId = Plan.PlanId; + Summary.FocusDeckId = Plan.FocusDeckId; + Summary.FocusMethodSegmentId = Plan.FocusMethodSegmentId; + Summary.LatestOutcomeScore = OutcomeSummary.OutcomeScore; + LatestSortUtc = SortUtc; + } + } + + if (Summary.PlanCount > 0) + { + Summary.AverageCompletionRate = CompletionRateAccumulator / static_cast(Summary.PlanCount); + Summary.AverageOutcomeScore = OutcomeScoreAccumulator / static_cast(Summary.PlanCount); + } + Summary.UnresolvedFollowUpCaseCount = OutstandingFollowUpCases.Num(); + Summary.bNeedsMoreFollowUp = + Summary.UnresolvedFollowUpCaseCount > 0 || bHasGenericFollowUpPressure || Summary.bHasOpenPlan; + + return Summary; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h index 9b71753..1193980 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h @@ -10,6 +10,8 @@ namespace HyperTwistTrainingRepositoryLibraryInternal FString ResolveReferenceUtc(const FString& ReferenceUtc); + int32 ComputeElapsedMilliseconds(const FString& OlderUtc, const FString& NewerUtc); + int32 ComputePlanDurationMs( const FHyperTwistTrainingReviewPlanState& ReviewPlan, const FString& ReferenceUtc diff --git a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md index 6fbda88..fa83346 100644 --- a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md +++ b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md @@ -1679,3 +1679,45 @@ Latest native repository coach-workflow split follow-up later on `2026-06-29`: same-family hotspot - the next worthwhile continuation remains another bounded training-repository seam rather than reopening product topology or renderer widening + +Latest native repository coach-summary split follow-up later on `2026-06-29`: + +- the next same-family continuation kept shrinking the remaining + training-repository hotspot without widening simulator or website scope: + - moved the coach action-plan summary, coach action-plan outcome summary, + and coach action-sequence summary family out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - added the extracted implementation unit at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp` + - widened the existing private internal declaration seam by one more bounded + helper declaration so the extracted summary family can reuse + `ComputeElapsedMilliseconds(...)` without reopening broader helper + exposure + - reduced the main repository file to `30,471` lines while the extracted + coach-summary unit now holds `625` 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.67 seconds` +- current truthful reading after this native follow-up: + - the coach-summary extraction is now build-proven on the canonical Windows + lane and safe to land + - vanilla refactoring still is not fully complete because the training + repository library still remains one of the real native hotspot files + - the next worthwhile continuation remains another bounded + training-repository or validation-library seam, not fresh product-family + widening diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md index 1e85e37..3ae67b7 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md @@ -2501,3 +2501,46 @@ Latest same-family responsive public-route expansion follow-up on `2026-06-29`: 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 + +## Latest native repository coach-summary split follow-up later on `2026-06-29` + +- the next same-family continuation then narrowed the remaining + training-repository hotspot again without widening simulator or website + scope: + - the coach action-plan summary, coach action-plan outcome summary, and + coach action-sequence summary family was moved out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - the extracted implementation family now lives at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp` + - the existing private helper declaration seam was widened by one more + bounded helper declaration so the extracted summary family can reuse + `ComputeElapsedMilliseconds(...)` without reopening broader helper + exposure + - the main repository file now stands at `30,471` lines and the extracted + coach-summary unit stands at `625` 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.67 seconds` +- current truthful reading after this native follow-up: + - the coach-summary extraction is now build-proven on the canonical Windows + lane + - vanilla refactoring still is not fully complete because the training + repository library remains one of the real native hotspot files + - the next worthwhile continuation remains another bounded + training-repository or validation-library seam instead of fresh + product-topology widening