diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 4209eb0..3b902f5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -24559,510 +24559,6 @@ FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingReposito return Summary; } -namespace HyperTwistTrainingRepositoryLibraryInternal -{ - FHyperTwistTrainingCoachActionClosureSummary DeriveCoachActionClosureSummaryWithCoachMemory( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachActionPlan& AnchorPlan, - const FString& ReferenceUtc, - const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot - ) - { - FHyperTwistTrainingCoachActionClosureSummary Summary; - const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); - if (!NormalizedAnchorPlan.IsStructurallyValid()) - { - return Summary; - } - - const FHyperTwistTrainingCoachActionSequenceSummary SequenceSummary = - UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary( - RepositoryState, - NormalizedAnchorPlan, - ReferenceUtc - ); - if (!SequenceSummary.IsStructurallyValid()) - { - return Summary; - } - - Summary.UserId = SequenceSummary.UserId; - Summary.ReferenceUtc = SequenceSummary.ReferenceUtc; - Summary.RootPlanId = SequenceSummary.RootPlanId; - Summary.LatestPlanId = SequenceSummary.LatestPlanId; - Summary.FocusDeckId = SequenceSummary.FocusDeckId; - Summary.FocusMethodSegmentId = SequenceSummary.FocusMethodSegmentId; - - TArray SequencePlans; - const TArray UserPlans = - UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser( - RepositoryState, - NormalizedAnchorPlan.UserId - ); - for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) - { - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); - const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() - ? NormalizedPlan.RootPlanId - : NormalizedPlan.PlanId; - if (CandidateRootPlanId == SequenceSummary.RootPlanId - || NormalizedPlan.PlanId == SequenceSummary.RootPlanId - || NormalizedPlan.ParentPlanId == SequenceSummary.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; - } - ); - - TSet AllFocusCaseIds; - TSet ResolvedCaseIds; - TSet UnresolvedCaseIds; - TMap CaseAttemptCounts; - TMap CaseNonSuccessCounts; - TMap CaseSuccessCounts; - bool bAnyRecognition = false; - - for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans) - { - const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary = - UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( - RepositoryState, - Plan, - SequenceSummary.ReferenceUtc - ); - TSet PlanFocusCaseIds; - for (const FString& FocusCaseId : Plan.FocusCaseIds) - { - if (!FocusCaseId.IsEmpty()) - { - PlanFocusCaseIds.Add(FocusCaseId); - AllFocusCaseIds.Add(FocusCaseId); - } - } - for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries) - { - if (!Entry.CaseId.IsEmpty()) - { - PlanFocusCaseIds.Add(Entry.CaseId); - AllFocusCaseIds.Add(Entry.CaseId); - } - } - - TSet PlanOutstandingCaseIds; - for (const FString& FollowUpCaseId : OutcomeSummary.FollowUpCaseIds) - { - if (!FollowUpCaseId.IsEmpty()) - { - PlanOutstandingCaseIds.Add(FollowUpCaseId); - UnresolvedCaseIds.Add(FollowUpCaseId); - ResolvedCaseIds.Remove(FollowUpCaseId); - } - } - for (const FString& DeferredCaseId : OutcomeSummary.DeferredLaunchCaseIds) - { - if (!DeferredCaseId.IsEmpty()) - { - PlanOutstandingCaseIds.Add(DeferredCaseId); - AllFocusCaseIds.Add(DeferredCaseId); - UnresolvedCaseIds.Add(DeferredCaseId); - ResolvedCaseIds.Remove(DeferredCaseId); - } - } - - for (const FString& FocusCaseId : PlanFocusCaseIds) - { - if (PlanOutstandingCaseIds.Contains(FocusCaseId)) - { - UnresolvedCaseIds.Add(FocusCaseId); - ResolvedCaseIds.Remove(FocusCaseId); - } - else - { - UnresolvedCaseIds.Remove(FocusCaseId); - ResolvedCaseIds.Add(FocusCaseId); - } - } - - FHyperTwistTrainingRunRecord RunRecord; - if (!Plan.SourceSessionId.IsEmpty() - && UHyperTwistTrainingRepositoryLibrary::TryGetRunRecord( - RepositoryState, - Plan.SourceSessionId, - RunRecord - )) - { - for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory) - { - if (!AllFocusCaseIds.Contains(AttemptHistoryEntry.CaseId)) - { - continue; - } - - Summary.TotalAttemptCount += 1; - CaseAttemptCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; - if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted - || AttemptHistoryEntry.RecognitionTimeMs > 0) - { - bAnyRecognition = true; - } - - if (AttemptHistoryEntry.Result == EHyperTwistTrainingAttemptResult::Success) - { - CaseSuccessCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; - } - else - { - CaseNonSuccessCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; - } - } - } - } - - for (const FString& UnresolvedCaseId : UnresolvedCaseIds) - { - ResolvedCaseIds.Remove(UnresolvedCaseId); - } - - TSet PersistentWeakCaseIds; - for (const TPair& Pair : CaseNonSuccessCounts) - { - const int32 SuccessCount = CaseSuccessCounts.FindRef(Pair.Key); - if (Pair.Value >= 2 - || (UnresolvedCaseIds.Contains(Pair.Key) && Pair.Value >= 1) - || (SuccessCount == 0 && CaseAttemptCounts.FindRef(Pair.Key) >= 2)) - { - PersistentWeakCaseIds.Add(Pair.Key); - } - } - - Summary.ResolvedCaseIds = ResolvedCaseIds.Array(); - Summary.PersistentWeakCaseIds = PersistentWeakCaseIds.Array(); - Summary.UnresolvedCaseIds = UnresolvedCaseIds.Array(); - Summary.ResolvedCaseIds.Sort(); - Summary.PersistentWeakCaseIds.Sort(); - Summary.UnresolvedCaseIds.Sort(); - Summary.ResolvedCaseCount = Summary.ResolvedCaseIds.Num(); - Summary.PersistentWeakCaseCount = Summary.PersistentWeakCaseIds.Num(); - Summary.UnresolvedCaseCount = Summary.UnresolvedCaseIds.Num(); - Summary.RepeatedFailureCaseCount = Summary.PersistentWeakCaseCount; - - Summary.bSequenceClosed = - !SequenceSummary.bHasOpenPlan - && !SequenceSummary.bNeedsMoreFollowUp - && Summary.UnresolvedCaseCount == 0 - && SequenceSummary.CompletedPlanCount > 0 - && SequenceSummary.AbortedPlanCount == 0; - Summary.bNeedsEscalation = - SequenceSummary.AbortedPlanCount > 0 - || (SequenceSummary.MaxFollowUpDepth >= 2 && Summary.UnresolvedCaseCount > 0) - || Summary.PersistentWeakCaseCount >= 2 - || SequenceSummary.AverageOutcomeScore < 30.0f; - const bool bPrefersFollowUpLane = - CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp - || CoachMemorySnapshot.FollowUpGuidanceContinueBias > CoachMemorySnapshot.RecommendedGuidanceContinueBias; - const bool bPrefersRecommendedLane = - CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended - || CoachMemorySnapshot.RecommendedGuidanceContinueBias > CoachMemorySnapshot.FollowUpGuidanceContinueBias; - const bool bHasRawFollowUpPressure = - Summary.UnresolvedCaseCount > 0 || Summary.PersistentWeakCaseCount > 0; - Summary.bSpawnFollowUpFromSuppressionMemory = - !bHasRawFollowUpPressure - && SequenceSummary.bNeedsMoreFollowUp - && SequenceSummary.CompletedPlanCount > 0 - && !Summary.bNeedsEscalation - && bPrefersFollowUpLane - && CoachMemorySnapshot.SuppressedPrimaryQueuePressureCount - >= CoachMemorySnapshot.SuppressedFollowUpQueuePressureCount; - Summary.bSuppressedBorderlineFollowUpByRecoveryHistory = - bHasRawFollowUpPressure - && Summary.PersistentWeakCaseCount == 0 - && Summary.UnresolvedCaseCount <= 1 - && !SequenceSummary.bNeedsMoreFollowUp - && !Summary.bNeedsEscalation - && bPrefersRecommendedLane - && CoachMemorySnapshot.SuppressedFollowUpQueuePressureCount - >= CoachMemorySnapshot.SuppressedPrimaryQueuePressureCount + 2; - Summary.bShouldSpawnFollowUp = - !Summary.bSequenceClosed - && !SequenceSummary.bHasOpenPlan - && !Summary.bSuppressedBorderlineFollowUpByRecoveryHistory - && (bHasRawFollowUpPressure || Summary.bSpawnFollowUpFromSuppressionMemory); - Summary.bShouldArchiveResolvedCases = - Summary.ResolvedCaseCount > 0 - && (Summary.bSequenceClosed || (!Summary.bShouldSpawnFollowUp && Summary.PersistentWeakCaseCount == 0)); - - if (Summary.bSequenceClosed) - { - Summary.ClosureLabel = TEXT("sequence-closed"); - Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::Timer; - } - else if (Summary.bNeedsEscalation) - { - Summary.ClosureLabel = TEXT("needs-escalation"); - Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::CoachReviewed; - } - else if (Summary.bShouldSpawnFollowUp) - { - Summary.ClosureLabel = Summary.bSpawnFollowUpFromSuppressionMemory - ? TEXT("follow-up-memory-ready") - : TEXT("follow-up-ready"); - Summary.RecommendedNextMode = bAnyRecognition - ? EHyperTwistTrainingDeliveryMode::RecognitionAssisted - : EHyperTwistTrainingDeliveryMode::VirtualCube; - } - else if (SequenceSummary.bHasOpenPlan) - { - Summary.ClosureLabel = TEXT("open-active"); - Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::CoachReviewed; - } - else - { - Summary.ClosureLabel = Summary.bSuppressedBorderlineFollowUpByRecoveryHistory - ? TEXT("recommended-recovery-stable") - : TEXT("sequence-stable"); - Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::Timer; - } - - Summary.ClosureScore = FMath::Clamp( - SequenceSummary.AverageOutcomeScore - + (static_cast(Summary.ResolvedCaseCount) * 6.0f) - - (static_cast(Summary.PersistentWeakCaseCount) * 8.0f) - - (static_cast(Summary.UnresolvedCaseCount) * 10.0f) - - (Summary.bNeedsEscalation ? 20.0f : 0.0f) - - (Summary.bSpawnFollowUpFromSuppressionMemory ? 6.0f : 0.0f) - + (Summary.bSuppressedBorderlineFollowUpByRecoveryHistory ? 4.0f : 0.0f), - 0.0f, - 100.0f - ); - - return Summary; - } -} - -FHyperTwistTrainingCoachActionClosureSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionClosureSummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FHyperTwistTrainingCoachActionPlan& AnchorPlan, - const FString& ReferenceUtc -) -{ - const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); - if (!NormalizedAnchorPlan.IsStructurallyValid()) - { - return FHyperTwistTrainingCoachActionClosureSummary(); - } - - const FString ResolvedReferenceUtc = - HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); - const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = - DeriveCoachMemorySnapshot(RepositoryState, NormalizedAnchorPlan.UserId, ResolvedReferenceUtc); - return HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( - RepositoryState, - NormalizedAnchorPlan, - ResolvedReferenceUtc, - CoachMemorySnapshot - ); -} - -FHyperTwistTrainingCoachClosureMemorySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachClosureMemorySummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachClosureMemorySummary Summary; - if (UserId.IsEmpty()) - { - return Summary; - } - - Summary.UserId = UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); - - const TArray UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId); - TMap LatestPlanByRootId; - TMap LatestSortUtcByRootId; - - for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) - { - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); - if (!NormalizedPlan.IsStructurallyValid()) - { - continue; - } - - const FString RootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() - ? NormalizedPlan.RootPlanId - : NormalizedPlan.PlanId; - const FString SortUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() - ? NormalizedPlan.CompletedAtUtc - : (!NormalizedPlan.StartedAtUtc.IsEmpty() ? NormalizedPlan.StartedAtUtc : NormalizedPlan.CreatedAtUtc); - - FHyperTwistTrainingCoachActionPlan* ExistingPlan = LatestPlanByRootId.Find(RootPlanId); - FString* ExistingSortUtc = LatestSortUtcByRootId.Find(RootPlanId); - if (ExistingPlan == nullptr || ExistingSortUtc == nullptr) - { - LatestPlanByRootId.Add(RootPlanId, NormalizedPlan); - LatestSortUtcByRootId.Add(RootPlanId, SortUtc); - continue; - } - - const int32 CompareResult = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, *ExistingSortUtc); - if (CompareResult > 0 - || (CompareResult == 0 && NormalizedPlan.FollowUpDepth >= ExistingPlan->FollowUpDepth)) - { - *ExistingPlan = NormalizedPlan; - *ExistingSortUtc = SortUtc; - } - } - - float ClosureScoreAccumulator = 0.0f; - int32 FocusPressureScore = -1; - FString FocusSortUtc; - TSet ArchivedResolvedCaseIds; - TSet PersistentWeakCaseIds; - TSet EscalationCaseIds; - const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = - HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot( - RepositoryState, - UserId, - Summary.ReferenceUtc - ); - - for (const TPair& Pair : LatestPlanByRootId) - { - const FHyperTwistTrainingCoachActionClosureSummary ClosureSummary = - HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( - RepositoryState, - Pair.Value, - Summary.ReferenceUtc, - CoachMemorySnapshot - ); - if (!ClosureSummary.IsStructurallyValid()) - { - continue; - } - - Summary.SequenceCount += 1; - Summary.ClosedSequenceCount += ClosureSummary.bSequenceClosed ? 1 : 0; - Summary.EscalatedSequenceCount += ClosureSummary.bNeedsEscalation ? 1 : 0; - Summary.FollowUpReadySequenceCount += ClosureSummary.bShouldSpawnFollowUp ? 1 : 0; - Summary.RecoverySustainedFollowUpSequenceCount += - ClosureSummary.bSpawnFollowUpFromSuppressionMemory ? 1 : 0; - Summary.RecoverySuppressedBorderlineFollowUpSequenceCount += - ClosureSummary.bSuppressedBorderlineFollowUpByRecoveryHistory ? 1 : 0; - ClosureScoreAccumulator += ClosureSummary.ClosureScore; - - if (ClosureSummary.bShouldArchiveResolvedCases) - { - for (const FString& CaseId : ClosureSummary.ResolvedCaseIds) - { - if (!CaseId.IsEmpty()) - { - ArchivedResolvedCaseIds.Add(CaseId); - } - } - } - for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) - { - if (!CaseId.IsEmpty()) - { - PersistentWeakCaseIds.Add(CaseId); - } - } - if (ClosureSummary.bNeedsEscalation) - { - for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) - { - if (!CaseId.IsEmpty()) - { - EscalationCaseIds.Add(CaseId); - } - } - for (const FString& CaseId : ClosureSummary.UnresolvedCaseIds) - { - if (!CaseId.IsEmpty()) - { - EscalationCaseIds.Add(CaseId); - } - } - } - - const int32 CurrentPressureScore = - (ClosureSummary.bNeedsEscalation ? 300 : 0) - + (ClosureSummary.bShouldSpawnFollowUp ? 200 : 0) - + (ClosureSummary.PersistentWeakCaseCount * 10) - + ClosureSummary.UnresolvedCaseCount; - const FString SortUtc = !ClosureSummary.ReferenceUtc.IsEmpty() - ? ClosureSummary.ReferenceUtc - : Summary.ReferenceUtc; - const int32 TimeCompare = FocusSortUtc.IsEmpty() - ? 1 - : HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, FocusSortUtc); - if (CurrentPressureScore > FocusPressureScore - || (CurrentPressureScore == FocusPressureScore && TimeCompare > 0)) - { - FocusPressureScore = CurrentPressureScore; - FocusSortUtc = SortUtc; - Summary.FocusRootPlanId = ClosureSummary.RootPlanId; - Summary.FocusDeckId = ClosureSummary.FocusDeckId; - Summary.FocusMethodSegmentId = ClosureSummary.FocusMethodSegmentId; - Summary.FocusClosureLabel = ClosureSummary.ClosureLabel; - } - } - - if (Summary.SequenceCount == 0) - { - return FHyperTwistTrainingCoachClosureMemorySummary(); - } - - Summary.ArchivedResolvedCaseIds = ArchivedResolvedCaseIds.Array(); - Summary.PersistentWeakCaseIds = PersistentWeakCaseIds.Array(); - Summary.EscalationCaseIds = EscalationCaseIds.Array(); - Summary.ArchivedResolvedCaseIds.Sort(); - Summary.PersistentWeakCaseIds.Sort(); - Summary.EscalationCaseIds.Sort(); - Summary.ArchivedResolvedCaseCount = Summary.ArchivedResolvedCaseIds.Num(); - Summary.PersistentWeakCaseCount = Summary.PersistentWeakCaseIds.Num(); - Summary.AverageClosureScore = ClosureScoreAccumulator / static_cast(Summary.SequenceCount); - Summary.bHasEscalationPressure = Summary.EscalatedSequenceCount > 0 || Summary.EscalationCaseIds.Num() > 0; - Summary.bHasFollowUpPressure = Summary.FollowUpReadySequenceCount > 0; - - return Summary; -} - FHyperTwistTrainingCoachRecommendationHistorySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachRecommendationHistorySummary( const FHyperTwistTrainingRepositoryState& RepositoryState, const FString& UserId, @@ -30022,441 +29518,6 @@ FHyperTwistTrainingCoachRecommendationHistorySummary UHyperTwistTrainingReposito return Summary; } -FHyperTwistTrainingCoachCarryForwardPolicySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachCarryForwardPolicySummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachCarryForwardPolicySummary Summary; - if (UserId.IsEmpty()) - { - return Summary; - } - - const FHyperTwistTrainingCoachClosureMemorySummary ClosureMemory = - DeriveCoachClosureMemorySummary(RepositoryState, UserId, ReferenceUtc); - const FHyperTwistTrainingCoachRecommendationHistorySummary RecommendationHistory = - DeriveCoachRecommendationHistorySummary(RepositoryState, UserId, ReferenceUtc); - if (!ClosureMemory.IsStructurallyValid() && !RecommendationHistory.IsStructurallyValid()) - { - return Summary; - } - - Summary.UserId = UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); - Summary.FocusDeckId = !RecommendationHistory.FocusDeckId.IsEmpty() - ? RecommendationHistory.FocusDeckId - : ClosureMemory.FocusDeckId; - Summary.FocusMethodSegmentId = !RecommendationHistory.FocusMethodSegmentId.IsEmpty() - ? RecommendationHistory.FocusMethodSegmentId - : ClosureMemory.FocusMethodSegmentId; - Summary.DominantSuggestedMode = RecommendationHistory.IsStructurallyValid() - ? RecommendationHistory.DominantSuggestedMode - : EHyperTwistTrainingDeliveryMode::Timer; - Summary.DominantSelectionPolicy = RecommendationHistory.IsStructurallyValid() - ? RecommendationHistory.DominantSelectionPolicy - : EHyperTwistTrainingSelectionPolicy::Weighted; - - TSet RecurringCarryForwardCaseIds; - TSet EscalationCarryForwardCaseIds; - TSet PinnedCarryForwardCaseIds; - TSet SuppressedResolvedCaseIds; - - for (const FString& CaseId : RecommendationHistory.RecurringCaseIds) - { - if (!CaseId.IsEmpty()) - { - RecurringCarryForwardCaseIds.Add(CaseId); - PinnedCarryForwardCaseIds.Add(CaseId); - } - } - for (const FString& CaseId : ClosureMemory.EscalationCaseIds) - { - if (!CaseId.IsEmpty()) - { - EscalationCarryForwardCaseIds.Add(CaseId); - PinnedCarryForwardCaseIds.Add(CaseId); - } - } - for (const FString& CaseId : RecommendationHistory.EscalationCaseIds) - { - if (!CaseId.IsEmpty()) - { - EscalationCarryForwardCaseIds.Add(CaseId); - PinnedCarryForwardCaseIds.Add(CaseId); - } - } - for (const FString& CaseId : ClosureMemory.PersistentWeakCaseIds) - { - if (!CaseId.IsEmpty()) - { - PinnedCarryForwardCaseIds.Add(CaseId); - } - } - for (const FString& CaseId : ClosureMemory.ArchivedResolvedCaseIds) - { - if (!CaseId.IsEmpty() && !PinnedCarryForwardCaseIds.Contains(CaseId)) - { - SuppressedResolvedCaseIds.Add(CaseId); - } - } - - Summary.RecurringCarryForwardCaseIds = RecurringCarryForwardCaseIds.Array(); - Summary.EscalationCarryForwardCaseIds = EscalationCarryForwardCaseIds.Array(); - Summary.PinnedCarryForwardCaseIds = PinnedCarryForwardCaseIds.Array(); - Summary.SuppressedResolvedCaseIds = SuppressedResolvedCaseIds.Array(); - Summary.RecurringCarryForwardCaseIds.Sort(); - Summary.EscalationCarryForwardCaseIds.Sort(); - Summary.PinnedCarryForwardCaseIds.Sort(); - Summary.SuppressedResolvedCaseIds.Sort(); - Summary.RecurringCarryForwardCaseCount = Summary.RecurringCarryForwardCaseIds.Num(); - Summary.EscalationCarryForwardCaseCount = Summary.EscalationCarryForwardCaseIds.Num(); - Summary.PinnedCarryForwardCaseCount = Summary.PinnedCarryForwardCaseIds.Num(); - Summary.SuppressedResolvedCaseCount = Summary.SuppressedResolvedCaseIds.Num(); - - Summary.bSuppressResolvedResurfacing = Summary.SuppressedResolvedCaseCount > 0; - Summary.bPinRecurringCases = Summary.PinnedCarryForwardCaseCount > 0; - Summary.bNeedsEscalationHandOff = - ClosureMemory.bHasEscalationPressure || Summary.EscalationCarryForwardCaseCount > 0; - Summary.bBiasCoachReviewed = - Summary.bNeedsEscalationHandOff - || RecommendationHistory.bShouldBiasCoachReviewed - || (Summary.PinnedCarryForwardCaseCount > 0 - && Summary.DominantSuggestedMode == EHyperTwistTrainingDeliveryMode::CoachReviewed); - Summary.bHasActiveCarryForwardPressure = - Summary.bNeedsEscalationHandOff - || Summary.bPinRecurringCases - || RecommendationHistory.bHasRepeatRecommendationPressure - || ClosureMemory.bHasFollowUpPressure; - - Summary.CarryForwardIntensityScore = FMath::Clamp( - (static_cast(Summary.EscalationCarryForwardCaseCount) * 25.0f) - + (static_cast(Summary.RecurringCarryForwardCaseCount) * 12.0f) - + (static_cast(RecommendationHistory.RepeatRecommendationSequenceCount) * 8.0f) - + (ClosureMemory.bHasFollowUpPressure ? 10.0f : 0.0f) - - (static_cast(Summary.SuppressedResolvedCaseCount) * 2.0f), - 0.0f, - 100.0f - ); - - return Summary; -} - -FHyperTwistTrainingCoachArchivePolicySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachArchivePolicySummary( - const FHyperTwistTrainingRepositoryState& RepositoryState, - const FString& UserId, - const FString& ReferenceUtc -) -{ - FHyperTwistTrainingCoachArchivePolicySummary Summary; - if (UserId.IsEmpty()) - { - return Summary; - } - - const FHyperTwistTrainingCoachClosureMemorySummary ClosureMemory = - DeriveCoachClosureMemorySummary(RepositoryState, UserId, ReferenceUtc); - const FHyperTwistTrainingCoachRecommendationHistorySummary RecommendationHistory = - DeriveCoachRecommendationHistorySummary(RepositoryState, UserId, ReferenceUtc); - const FHyperTwistTrainingCoachCarryForwardPolicySummary CarryForwardPolicy = - DeriveCoachCarryForwardPolicySummary(RepositoryState, UserId, ReferenceUtc); - if (!ClosureMemory.IsStructurallyValid() - && !RecommendationHistory.IsStructurallyValid() - && !CarryForwardPolicy.IsStructurallyValid()) - { - return Summary; - } - - Summary.UserId = UserId; - Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); - Summary.FocusDeckId = CarryForwardPolicy.IsStructurallyValid() - ? CarryForwardPolicy.FocusDeckId - : (!RecommendationHistory.FocusDeckId.IsEmpty() ? RecommendationHistory.FocusDeckId : ClosureMemory.FocusDeckId); - Summary.FocusMethodSegmentId = CarryForwardPolicy.IsStructurallyValid() - ? CarryForwardPolicy.FocusMethodSegmentId - : (!RecommendationHistory.FocusMethodSegmentId.IsEmpty() - ? RecommendationHistory.FocusMethodSegmentId - : ClosureMemory.FocusMethodSegmentId); - Summary.DominantSuggestedMode = CarryForwardPolicy.IsStructurallyValid() - ? CarryForwardPolicy.DominantSuggestedMode - : RecommendationHistory.DominantSuggestedMode; - Summary.DominantSelectionPolicy = CarryForwardPolicy.IsStructurallyValid() - ? CarryForwardPolicy.DominantSelectionPolicy - : RecommendationHistory.DominantSelectionPolicy; - - const TArray UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId); - TMap LatestPlanByRootId; - TMap LatestSortUtcByRootId; - - for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) - { - const FHyperTwistTrainingCoachActionPlan NormalizedPlan = - HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); - if (!NormalizedPlan.IsStructurallyValid()) - { - continue; - } - - const FString RootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() - ? NormalizedPlan.RootPlanId - : NormalizedPlan.PlanId; - const FString SortUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() - ? NormalizedPlan.CompletedAtUtc - : (!NormalizedPlan.StartedAtUtc.IsEmpty() ? NormalizedPlan.StartedAtUtc : NormalizedPlan.CreatedAtUtc); - - FHyperTwistTrainingCoachActionPlan* ExistingPlan = LatestPlanByRootId.Find(RootPlanId); - FString* ExistingSortUtc = LatestSortUtcByRootId.Find(RootPlanId); - if (ExistingPlan == nullptr || ExistingSortUtc == nullptr) - { - LatestPlanByRootId.Add(RootPlanId, NormalizedPlan); - LatestSortUtcByRootId.Add(RootPlanId, SortUtc); - continue; - } - - const int32 CompareResult = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, *ExistingSortUtc); - if (CompareResult > 0 - || (CompareResult == 0 && NormalizedPlan.FollowUpDepth >= ExistingPlan->FollowUpDepth)) - { - *ExistingPlan = NormalizedPlan; - *ExistingSortUtc = SortUtc; - } - } - - TMap ResolvedArchiveHitCounts; - TMap OpenCarryForwardHitCounts; - TMap EscalationHitCounts; - TMap StableArchiveHitCounts; - const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = - HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot( - RepositoryState, - UserId, - Summary.ReferenceUtc - ); - - for (const TPair& Pair : LatestPlanByRootId) - { - const FHyperTwistTrainingCoachActionClosureSummary ClosureSummary = - HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( - RepositoryState, - Pair.Value, - Summary.ReferenceUtc, - CoachMemorySnapshot - ); - if (!ClosureSummary.IsStructurallyValid()) - { - continue; - } - - if (ClosureSummary.bShouldArchiveResolvedCases) - { - for (const FString& CaseId : ClosureSummary.ResolvedCaseIds) - { - if (CaseId.IsEmpty()) - { - continue; - } - - ResolvedArchiveHitCounts.FindOrAdd(CaseId) += 1; - if (ClosureSummary.bSequenceClosed - && !ClosureSummary.bNeedsEscalation - && !ClosureSummary.bShouldSpawnFollowUp) - { - StableArchiveHitCounts.FindOrAdd(CaseId) += 1; - } - } - } - - for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) - { - if (!CaseId.IsEmpty()) - { - OpenCarryForwardHitCounts.FindOrAdd(CaseId) += 1; - if (ClosureSummary.bNeedsEscalation) - { - EscalationHitCounts.FindOrAdd(CaseId) += 1; - } - } - } - for (const FString& CaseId : ClosureSummary.UnresolvedCaseIds) - { - if (!CaseId.IsEmpty()) - { - OpenCarryForwardHitCounts.FindOrAdd(CaseId) += 1; - if (ClosureSummary.bNeedsEscalation) - { - EscalationHitCounts.FindOrAdd(CaseId) += 1; - } - } - } - } - - TSet RecurringCaseIds; - for (const FString& CaseId : RecommendationHistory.RecurringCaseIds) - { - if (!CaseId.IsEmpty()) - { - RecurringCaseIds.Add(CaseId); - } - } - - TSet LongTermArchivedCaseIds; - TSet ExpiredCarryForwardCaseIds; - TSet RetainedCarryForwardCaseIds; - TSet EscalationRetainedCaseIds; - TSet SuppressedResolvedCaseIds; - TSet StableLongTermArchivedCaseIds; - TSet ProvisionalArchivedCaseIds; - TSet ReopenRiskArchivedCaseIds; - - for (const FString& CaseId : CarryForwardPolicy.SuppressedResolvedCaseIds) - { - if (!CaseId.IsEmpty()) - { - LongTermArchivedCaseIds.Add(CaseId); - SuppressedResolvedCaseIds.Add(CaseId); - - const int32 OpenCarryForwardHits = OpenCarryForwardHitCounts.FindRef(CaseId); - const int32 StableArchiveHits = StableArchiveHitCounts.FindRef(CaseId); - const bool bRecurringCase = RecurringCaseIds.Contains(CaseId); - if (StableArchiveHits > 0 && OpenCarryForwardHits == 0 && !bRecurringCase) - { - StableLongTermArchivedCaseIds.Add(CaseId); - } - else - { - ProvisionalArchivedCaseIds.Add(CaseId); - if (bRecurringCase || OpenCarryForwardHits > 0) - { - ReopenRiskArchivedCaseIds.Add(CaseId); - } - } - } - } - - const bool bStableClosureMemory = - ClosureMemory.AverageClosureScore >= 70.0f - && !ClosureMemory.bHasEscalationPressure - && !ClosureMemory.bHasFollowUpPressure; - - for (const FString& CaseId : CarryForwardPolicy.PinnedCarryForwardCaseIds) - { - if (CaseId.IsEmpty()) - { - continue; - } - - const int32 ResolvedArchiveHits = ResolvedArchiveHitCounts.FindRef(CaseId); - const int32 OpenCarryForwardHits = OpenCarryForwardHitCounts.FindRef(CaseId); - const int32 EscalationHits = EscalationHitCounts.FindRef(CaseId); - const int32 StableArchiveHits = StableArchiveHitCounts.FindRef(CaseId); - const bool bRecurringCase = RecurringCaseIds.Contains(CaseId); - - const bool bCanExpireResolvedCarryForward = - EscalationHits == 0 - && ResolvedArchiveHits > 0 - && OpenCarryForwardHits <= ResolvedArchiveHits - && (StableArchiveHits > 0 - || bStableClosureMemory - || (ResolvedArchiveHits >= 2 && !bRecurringCase) - || (ResolvedArchiveHits > OpenCarryForwardHits - && RecommendationHistory.RepeatRecommendationSequenceCount <= 1)); - - if (bCanExpireResolvedCarryForward) - { - ExpiredCarryForwardCaseIds.Add(CaseId); - LongTermArchivedCaseIds.Add(CaseId); - if (StableArchiveHits > 0 && !bRecurringCase && OpenCarryForwardHits == 0) - { - StableLongTermArchivedCaseIds.Add(CaseId); - } - else - { - ProvisionalArchivedCaseIds.Add(CaseId); - if (bRecurringCase - || RecommendationHistory.bHasRepeatRecommendationPressure - || OpenCarryForwardHits > 0) - { - ReopenRiskArchivedCaseIds.Add(CaseId); - } - } - continue; - } - - RetainedCarryForwardCaseIds.Add(CaseId); - if (EscalationHits > 0) - { - EscalationRetainedCaseIds.Add(CaseId); - } - } - - Summary.LongTermArchivedCaseIds = LongTermArchivedCaseIds.Array(); - Summary.ExpiredCarryForwardCaseIds = ExpiredCarryForwardCaseIds.Array(); - Summary.RetainedCarryForwardCaseIds = RetainedCarryForwardCaseIds.Array(); - Summary.EscalationRetainedCaseIds = EscalationRetainedCaseIds.Array(); - Summary.SuppressedResolvedCaseIds = SuppressedResolvedCaseIds.Array(); - Summary.StableLongTermArchivedCaseIds = StableLongTermArchivedCaseIds.Array(); - Summary.ProvisionalArchivedCaseIds = ProvisionalArchivedCaseIds.Array(); - Summary.ReopenRiskArchivedCaseIds = ReopenRiskArchivedCaseIds.Array(); - Summary.LongTermArchivedCaseIds.Sort(); - Summary.ExpiredCarryForwardCaseIds.Sort(); - Summary.RetainedCarryForwardCaseIds.Sort(); - Summary.EscalationRetainedCaseIds.Sort(); - Summary.SuppressedResolvedCaseIds.Sort(); - Summary.StableLongTermArchivedCaseIds.Sort(); - Summary.ProvisionalArchivedCaseIds.Sort(); - Summary.ReopenRiskArchivedCaseIds.Sort(); - - Summary.LongTermArchivedCaseCount = Summary.LongTermArchivedCaseIds.Num(); - Summary.ExpiredCarryForwardCaseCount = Summary.ExpiredCarryForwardCaseIds.Num(); - Summary.RetainedCarryForwardCaseCount = Summary.RetainedCarryForwardCaseIds.Num(); - Summary.EscalationRetainedCaseCount = Summary.EscalationRetainedCaseIds.Num(); - Summary.SuppressedResolvedCaseCount = Summary.SuppressedResolvedCaseIds.Num(); - Summary.StableLongTermArchivedCaseCount = Summary.StableLongTermArchivedCaseIds.Num(); - Summary.ProvisionalArchivedCaseCount = Summary.ProvisionalArchivedCaseIds.Num(); - Summary.ReopenRiskArchivedCaseCount = Summary.ReopenRiskArchivedCaseIds.Num(); - - Summary.bShouldExpireResolvedCarryForward = Summary.ExpiredCarryForwardCaseCount > 0; - Summary.bShouldRetainEscalationCarryForward = Summary.EscalationRetainedCaseCount > 0; - Summary.ArchiveStabilityScore = FMath::Clamp( - ClosureMemory.AverageClosureScore - + (static_cast(Summary.LongTermArchivedCaseCount) * 5.0f) - + (static_cast(Summary.StableLongTermArchivedCaseCount) * 4.0f) - + (bStableClosureMemory ? 10.0f : 0.0f) - - (static_cast(Summary.ProvisionalArchivedCaseCount) * 3.0f) - - (static_cast(Summary.ReopenRiskArchivedCaseCount) * 8.0f) - - (static_cast(Summary.RetainedCarryForwardCaseCount) * 4.0f) - - (static_cast(Summary.EscalationRetainedCaseCount) * 10.0f) - - (RecommendationHistory.bHasRepeatRecommendationPressure ? 6.0f : 0.0f), - 0.0f, - 100.0f - ); - Summary.ArchiveRetentionTuningScore = FMath::Clamp( - (static_cast(Summary.ReopenRiskArchivedCaseCount) * 18.0f) - + (static_cast(Summary.ProvisionalArchivedCaseCount) * 9.0f) - + (RecommendationHistory.bHasRepeatRecommendationPressure ? 12.0f : 0.0f) - + (Summary.bShouldRetainEscalationCarryForward ? 14.0f : 0.0f) - - (static_cast(Summary.StableLongTermArchivedCaseCount) * 6.0f), - 0.0f, - 100.0f - ); - Summary.bHasStableArchiveMemory = - Summary.LongTermArchivedCaseCount > 0 - && Summary.ArchiveStabilityScore >= 55.0f - && Summary.EscalationRetainedCaseCount == 0; - Summary.bNeedsArchiveRetentionTuning = - Summary.ReopenRiskArchivedCaseCount > 0 - || Summary.ArchiveRetentionTuningScore >= 25.0f - || (Summary.ProvisionalArchivedCaseCount > Summary.StableLongTermArchivedCaseCount - && Summary.LongTermArchivedCaseCount > 0); - Summary.bHasActiveArchivePressure = - Summary.RetainedCarryForwardCaseCount > 0 - || Summary.EscalationRetainedCaseCount > 0 - || Summary.bNeedsArchiveRetentionTuning; - - return Summary; -} - FHyperTwistTrainingCaseRecommendation UHyperTwistTrainingRepositoryLibrary::MakeCaseRecommendation( const FHyperTwistTrainingCase& TrainingCase, const FHyperTwistTrainingCaseLearnerState& LearnerState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachPolicies.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachPolicies.cpp new file mode 100644 index 0000000..c7e9bbe --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachPolicies.cpp @@ -0,0 +1,946 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" + +#include "HyperTwistTrainingRepositoryLibraryInternal.h" + +namespace HyperTwistTrainingRepositoryLibraryInternal +{ + FHyperTwistTrainingCoachActionClosureSummary DeriveCoachActionClosureSummaryWithCoachMemory( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& AnchorPlan, + const FString& ReferenceUtc, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot + ) + { + FHyperTwistTrainingCoachActionClosureSummary Summary; + const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); + if (!NormalizedAnchorPlan.IsStructurallyValid()) + { + return Summary; + } + + const FHyperTwistTrainingCoachActionSequenceSummary SequenceSummary = + UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary( + RepositoryState, + NormalizedAnchorPlan, + ReferenceUtc + ); + if (!SequenceSummary.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = SequenceSummary.UserId; + Summary.ReferenceUtc = SequenceSummary.ReferenceUtc; + Summary.RootPlanId = SequenceSummary.RootPlanId; + Summary.LatestPlanId = SequenceSummary.LatestPlanId; + Summary.FocusDeckId = SequenceSummary.FocusDeckId; + Summary.FocusMethodSegmentId = SequenceSummary.FocusMethodSegmentId; + + TArray SequencePlans; + const TArray UserPlans = + UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser( + RepositoryState, + NormalizedAnchorPlan.UserId + ); + for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) + { + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); + const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() + ? NormalizedPlan.RootPlanId + : NormalizedPlan.PlanId; + if (CandidateRootPlanId == SequenceSummary.RootPlanId + || NormalizedPlan.PlanId == SequenceSummary.RootPlanId + || NormalizedPlan.ParentPlanId == SequenceSummary.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; + } + ); + + TSet AllFocusCaseIds; + TSet ResolvedCaseIds; + TSet UnresolvedCaseIds; + TMap CaseAttemptCounts; + TMap CaseNonSuccessCounts; + TMap CaseSuccessCounts; + bool bAnyRecognition = false; + + for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans) + { + const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary = + UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( + RepositoryState, + Plan, + SequenceSummary.ReferenceUtc + ); + TSet PlanFocusCaseIds; + for (const FString& FocusCaseId : Plan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty()) + { + PlanFocusCaseIds.Add(FocusCaseId); + AllFocusCaseIds.Add(FocusCaseId); + } + } + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries) + { + if (!Entry.CaseId.IsEmpty()) + { + PlanFocusCaseIds.Add(Entry.CaseId); + AllFocusCaseIds.Add(Entry.CaseId); + } + } + + TSet PlanOutstandingCaseIds; + for (const FString& FollowUpCaseId : OutcomeSummary.FollowUpCaseIds) + { + if (!FollowUpCaseId.IsEmpty()) + { + PlanOutstandingCaseIds.Add(FollowUpCaseId); + UnresolvedCaseIds.Add(FollowUpCaseId); + ResolvedCaseIds.Remove(FollowUpCaseId); + } + } + for (const FString& DeferredCaseId : OutcomeSummary.DeferredLaunchCaseIds) + { + if (!DeferredCaseId.IsEmpty()) + { + PlanOutstandingCaseIds.Add(DeferredCaseId); + AllFocusCaseIds.Add(DeferredCaseId); + UnresolvedCaseIds.Add(DeferredCaseId); + ResolvedCaseIds.Remove(DeferredCaseId); + } + } + + for (const FString& FocusCaseId : PlanFocusCaseIds) + { + if (PlanOutstandingCaseIds.Contains(FocusCaseId)) + { + UnresolvedCaseIds.Add(FocusCaseId); + ResolvedCaseIds.Remove(FocusCaseId); + } + else + { + UnresolvedCaseIds.Remove(FocusCaseId); + ResolvedCaseIds.Add(FocusCaseId); + } + } + + FHyperTwistTrainingRunRecord RunRecord; + if (!Plan.SourceSessionId.IsEmpty() + && UHyperTwistTrainingRepositoryLibrary::TryGetRunRecord( + RepositoryState, + Plan.SourceSessionId, + RunRecord + )) + { + for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory) + { + if (!AllFocusCaseIds.Contains(AttemptHistoryEntry.CaseId)) + { + continue; + } + + Summary.TotalAttemptCount += 1; + CaseAttemptCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; + if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted + || AttemptHistoryEntry.RecognitionTimeMs > 0) + { + bAnyRecognition = true; + } + + if (AttemptHistoryEntry.Result == EHyperTwistTrainingAttemptResult::Success) + { + CaseSuccessCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; + } + else + { + CaseNonSuccessCounts.FindOrAdd(AttemptHistoryEntry.CaseId) += 1; + } + } + } + } + + for (const FString& UnresolvedCaseId : UnresolvedCaseIds) + { + ResolvedCaseIds.Remove(UnresolvedCaseId); + } + + TSet PersistentWeakCaseIds; + for (const TPair& Pair : CaseNonSuccessCounts) + { + const int32 SuccessCount = CaseSuccessCounts.FindRef(Pair.Key); + if (Pair.Value >= 2 + || (UnresolvedCaseIds.Contains(Pair.Key) && Pair.Value >= 1) + || (SuccessCount == 0 && CaseAttemptCounts.FindRef(Pair.Key) >= 2)) + { + PersistentWeakCaseIds.Add(Pair.Key); + } + } + + Summary.ResolvedCaseIds = ResolvedCaseIds.Array(); + Summary.PersistentWeakCaseIds = PersistentWeakCaseIds.Array(); + Summary.UnresolvedCaseIds = UnresolvedCaseIds.Array(); + Summary.ResolvedCaseIds.Sort(); + Summary.PersistentWeakCaseIds.Sort(); + Summary.UnresolvedCaseIds.Sort(); + Summary.ResolvedCaseCount = Summary.ResolvedCaseIds.Num(); + Summary.PersistentWeakCaseCount = Summary.PersistentWeakCaseIds.Num(); + Summary.UnresolvedCaseCount = Summary.UnresolvedCaseIds.Num(); + Summary.RepeatedFailureCaseCount = Summary.PersistentWeakCaseCount; + + Summary.bSequenceClosed = + !SequenceSummary.bHasOpenPlan + && !SequenceSummary.bNeedsMoreFollowUp + && Summary.UnresolvedCaseCount == 0 + && SequenceSummary.CompletedPlanCount > 0 + && SequenceSummary.AbortedPlanCount == 0; + Summary.bNeedsEscalation = + SequenceSummary.AbortedPlanCount > 0 + || (SequenceSummary.MaxFollowUpDepth >= 2 && Summary.UnresolvedCaseCount > 0) + || Summary.PersistentWeakCaseCount >= 2 + || SequenceSummary.AverageOutcomeScore < 30.0f; + const bool bPrefersFollowUpLane = + CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp + || CoachMemorySnapshot.FollowUpGuidanceContinueBias > CoachMemorySnapshot.RecommendedGuidanceContinueBias; + const bool bPrefersRecommendedLane = + CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended + || CoachMemorySnapshot.RecommendedGuidanceContinueBias > CoachMemorySnapshot.FollowUpGuidanceContinueBias; + const bool bHasRawFollowUpPressure = + Summary.UnresolvedCaseCount > 0 || Summary.PersistentWeakCaseCount > 0; + Summary.bSpawnFollowUpFromSuppressionMemory = + !bHasRawFollowUpPressure + && SequenceSummary.bNeedsMoreFollowUp + && SequenceSummary.CompletedPlanCount > 0 + && !Summary.bNeedsEscalation + && bPrefersFollowUpLane + && CoachMemorySnapshot.SuppressedPrimaryQueuePressureCount + >= CoachMemorySnapshot.SuppressedFollowUpQueuePressureCount; + Summary.bSuppressedBorderlineFollowUpByRecoveryHistory = + bHasRawFollowUpPressure + && Summary.PersistentWeakCaseCount == 0 + && Summary.UnresolvedCaseCount <= 1 + && !SequenceSummary.bNeedsMoreFollowUp + && !Summary.bNeedsEscalation + && bPrefersRecommendedLane + && CoachMemorySnapshot.SuppressedFollowUpQueuePressureCount + >= CoachMemorySnapshot.SuppressedPrimaryQueuePressureCount + 2; + Summary.bShouldSpawnFollowUp = + !Summary.bSequenceClosed + && !SequenceSummary.bHasOpenPlan + && !Summary.bSuppressedBorderlineFollowUpByRecoveryHistory + && (bHasRawFollowUpPressure || Summary.bSpawnFollowUpFromSuppressionMemory); + Summary.bShouldArchiveResolvedCases = + Summary.ResolvedCaseCount > 0 + && (Summary.bSequenceClosed || (!Summary.bShouldSpawnFollowUp && Summary.PersistentWeakCaseCount == 0)); + + if (Summary.bSequenceClosed) + { + Summary.ClosureLabel = TEXT("sequence-closed"); + Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::Timer; + } + else if (Summary.bNeedsEscalation) + { + Summary.ClosureLabel = TEXT("needs-escalation"); + Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::CoachReviewed; + } + else if (Summary.bShouldSpawnFollowUp) + { + Summary.ClosureLabel = Summary.bSpawnFollowUpFromSuppressionMemory + ? TEXT("follow-up-memory-ready") + : TEXT("follow-up-ready"); + Summary.RecommendedNextMode = bAnyRecognition + ? EHyperTwistTrainingDeliveryMode::RecognitionAssisted + : EHyperTwistTrainingDeliveryMode::VirtualCube; + } + else if (SequenceSummary.bHasOpenPlan) + { + Summary.ClosureLabel = TEXT("open-active"); + Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::CoachReviewed; + } + else + { + Summary.ClosureLabel = Summary.bSuppressedBorderlineFollowUpByRecoveryHistory + ? TEXT("recommended-recovery-stable") + : TEXT("sequence-stable"); + Summary.RecommendedNextMode = EHyperTwistTrainingDeliveryMode::Timer; + } + + Summary.ClosureScore = FMath::Clamp( + SequenceSummary.AverageOutcomeScore + + (static_cast(Summary.ResolvedCaseCount) * 6.0f) + - (static_cast(Summary.PersistentWeakCaseCount) * 8.0f) + - (static_cast(Summary.UnresolvedCaseCount) * 10.0f) + - (Summary.bNeedsEscalation ? 20.0f : 0.0f) + - (Summary.bSpawnFollowUpFromSuppressionMemory ? 6.0f : 0.0f) + + (Summary.bSuppressedBorderlineFollowUpByRecoveryHistory ? 4.0f : 0.0f), + 0.0f, + 100.0f + ); + + return Summary; + } +} + +FHyperTwistTrainingCoachActionClosureSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionClosureSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& AnchorPlan, + const FString& ReferenceUtc +) +{ + const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan); + if (!NormalizedAnchorPlan.IsStructurallyValid()) + { + return FHyperTwistTrainingCoachActionClosureSummary(); + } + + const FString ResolvedReferenceUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = + DeriveCoachMemorySnapshot(RepositoryState, NormalizedAnchorPlan.UserId, ResolvedReferenceUtc); + return HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( + RepositoryState, + NormalizedAnchorPlan, + ResolvedReferenceUtc, + CoachMemorySnapshot + ); +} + +FHyperTwistTrainingCoachClosureMemorySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachClosureMemorySummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachClosureMemorySummary Summary; + if (UserId.IsEmpty()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + + const TArray UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId); + TMap LatestPlanByRootId; + TMap LatestSortUtcByRootId; + + for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) + { + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); + if (!NormalizedPlan.IsStructurallyValid()) + { + continue; + } + + const FString RootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() + ? NormalizedPlan.RootPlanId + : NormalizedPlan.PlanId; + const FString SortUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() + ? NormalizedPlan.CompletedAtUtc + : (!NormalizedPlan.StartedAtUtc.IsEmpty() ? NormalizedPlan.StartedAtUtc : NormalizedPlan.CreatedAtUtc); + + FHyperTwistTrainingCoachActionPlan* ExistingPlan = LatestPlanByRootId.Find(RootPlanId); + FString* ExistingSortUtc = LatestSortUtcByRootId.Find(RootPlanId); + if (ExistingPlan == nullptr || ExistingSortUtc == nullptr) + { + LatestPlanByRootId.Add(RootPlanId, NormalizedPlan); + LatestSortUtcByRootId.Add(RootPlanId, SortUtc); + continue; + } + + const int32 CompareResult = + HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, *ExistingSortUtc); + if (CompareResult > 0 + || (CompareResult == 0 && NormalizedPlan.FollowUpDepth >= ExistingPlan->FollowUpDepth)) + { + *ExistingPlan = NormalizedPlan; + *ExistingSortUtc = SortUtc; + } + } + + float ClosureScoreAccumulator = 0.0f; + int32 FocusPressureScore = -1; + FString FocusSortUtc; + TSet ArchivedResolvedCaseIds; + TSet PersistentWeakCaseIds; + TSet EscalationCaseIds; + const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = + HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot( + RepositoryState, + UserId, + Summary.ReferenceUtc + ); + + for (const TPair& Pair : LatestPlanByRootId) + { + const FHyperTwistTrainingCoachActionClosureSummary ClosureSummary = + HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( + RepositoryState, + Pair.Value, + Summary.ReferenceUtc, + CoachMemorySnapshot + ); + if (!ClosureSummary.IsStructurallyValid()) + { + continue; + } + + Summary.SequenceCount += 1; + Summary.ClosedSequenceCount += ClosureSummary.bSequenceClosed ? 1 : 0; + Summary.EscalatedSequenceCount += ClosureSummary.bNeedsEscalation ? 1 : 0; + Summary.FollowUpReadySequenceCount += ClosureSummary.bShouldSpawnFollowUp ? 1 : 0; + Summary.RecoverySustainedFollowUpSequenceCount += + ClosureSummary.bSpawnFollowUpFromSuppressionMemory ? 1 : 0; + Summary.RecoverySuppressedBorderlineFollowUpSequenceCount += + ClosureSummary.bSuppressedBorderlineFollowUpByRecoveryHistory ? 1 : 0; + ClosureScoreAccumulator += ClosureSummary.ClosureScore; + + if (ClosureSummary.bShouldArchiveResolvedCases) + { + for (const FString& CaseId : ClosureSummary.ResolvedCaseIds) + { + if (!CaseId.IsEmpty()) + { + ArchivedResolvedCaseIds.Add(CaseId); + } + } + } + for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) + { + if (!CaseId.IsEmpty()) + { + PersistentWeakCaseIds.Add(CaseId); + } + } + if (ClosureSummary.bNeedsEscalation) + { + for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) + { + if (!CaseId.IsEmpty()) + { + EscalationCaseIds.Add(CaseId); + } + } + for (const FString& CaseId : ClosureSummary.UnresolvedCaseIds) + { + if (!CaseId.IsEmpty()) + { + EscalationCaseIds.Add(CaseId); + } + } + } + + const int32 CurrentPressureScore = + (ClosureSummary.bNeedsEscalation ? 300 : 0) + + (ClosureSummary.bShouldSpawnFollowUp ? 200 : 0) + + (ClosureSummary.PersistentWeakCaseCount * 10) + + ClosureSummary.UnresolvedCaseCount; + const FString SortUtc = !ClosureSummary.ReferenceUtc.IsEmpty() + ? ClosureSummary.ReferenceUtc + : Summary.ReferenceUtc; + const int32 TimeCompare = FocusSortUtc.IsEmpty() + ? 1 + : HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, FocusSortUtc); + if (CurrentPressureScore > FocusPressureScore + || (CurrentPressureScore == FocusPressureScore && TimeCompare > 0)) + { + FocusPressureScore = CurrentPressureScore; + FocusSortUtc = SortUtc; + Summary.FocusRootPlanId = ClosureSummary.RootPlanId; + Summary.FocusDeckId = ClosureSummary.FocusDeckId; + Summary.FocusMethodSegmentId = ClosureSummary.FocusMethodSegmentId; + Summary.FocusClosureLabel = ClosureSummary.ClosureLabel; + } + } + + if (Summary.SequenceCount == 0) + { + return FHyperTwistTrainingCoachClosureMemorySummary(); + } + + Summary.ArchivedResolvedCaseIds = ArchivedResolvedCaseIds.Array(); + Summary.PersistentWeakCaseIds = PersistentWeakCaseIds.Array(); + Summary.EscalationCaseIds = EscalationCaseIds.Array(); + Summary.ArchivedResolvedCaseIds.Sort(); + Summary.PersistentWeakCaseIds.Sort(); + Summary.EscalationCaseIds.Sort(); + Summary.ArchivedResolvedCaseCount = Summary.ArchivedResolvedCaseIds.Num(); + Summary.PersistentWeakCaseCount = Summary.PersistentWeakCaseIds.Num(); + Summary.AverageClosureScore = ClosureScoreAccumulator / static_cast(Summary.SequenceCount); + Summary.bHasEscalationPressure = Summary.EscalatedSequenceCount > 0 || Summary.EscalationCaseIds.Num() > 0; + Summary.bHasFollowUpPressure = Summary.FollowUpReadySequenceCount > 0; + + return Summary; +} + +FHyperTwistTrainingCoachCarryForwardPolicySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachCarryForwardPolicySummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachCarryForwardPolicySummary Summary; + if (UserId.IsEmpty()) + { + return Summary; + } + + const FHyperTwistTrainingCoachClosureMemorySummary ClosureMemory = + DeriveCoachClosureMemorySummary(RepositoryState, UserId, ReferenceUtc); + const FHyperTwistTrainingCoachRecommendationHistorySummary RecommendationHistory = + DeriveCoachRecommendationHistorySummary(RepositoryState, UserId, ReferenceUtc); + if (!ClosureMemory.IsStructurallyValid() && !RecommendationHistory.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + Summary.FocusDeckId = !RecommendationHistory.FocusDeckId.IsEmpty() + ? RecommendationHistory.FocusDeckId + : ClosureMemory.FocusDeckId; + Summary.FocusMethodSegmentId = !RecommendationHistory.FocusMethodSegmentId.IsEmpty() + ? RecommendationHistory.FocusMethodSegmentId + : ClosureMemory.FocusMethodSegmentId; + Summary.DominantSuggestedMode = RecommendationHistory.IsStructurallyValid() + ? RecommendationHistory.DominantSuggestedMode + : EHyperTwistTrainingDeliveryMode::Timer; + Summary.DominantSelectionPolicy = RecommendationHistory.IsStructurallyValid() + ? RecommendationHistory.DominantSelectionPolicy + : EHyperTwistTrainingSelectionPolicy::Weighted; + + TSet RecurringCarryForwardCaseIds; + TSet EscalationCarryForwardCaseIds; + TSet PinnedCarryForwardCaseIds; + TSet SuppressedResolvedCaseIds; + + for (const FString& CaseId : RecommendationHistory.RecurringCaseIds) + { + if (!CaseId.IsEmpty()) + { + RecurringCarryForwardCaseIds.Add(CaseId); + PinnedCarryForwardCaseIds.Add(CaseId); + } + } + for (const FString& CaseId : ClosureMemory.EscalationCaseIds) + { + if (!CaseId.IsEmpty()) + { + EscalationCarryForwardCaseIds.Add(CaseId); + PinnedCarryForwardCaseIds.Add(CaseId); + } + } + for (const FString& CaseId : RecommendationHistory.EscalationCaseIds) + { + if (!CaseId.IsEmpty()) + { + EscalationCarryForwardCaseIds.Add(CaseId); + PinnedCarryForwardCaseIds.Add(CaseId); + } + } + for (const FString& CaseId : ClosureMemory.PersistentWeakCaseIds) + { + if (!CaseId.IsEmpty()) + { + PinnedCarryForwardCaseIds.Add(CaseId); + } + } + for (const FString& CaseId : ClosureMemory.ArchivedResolvedCaseIds) + { + if (!CaseId.IsEmpty() && !PinnedCarryForwardCaseIds.Contains(CaseId)) + { + SuppressedResolvedCaseIds.Add(CaseId); + } + } + + Summary.RecurringCarryForwardCaseIds = RecurringCarryForwardCaseIds.Array(); + Summary.EscalationCarryForwardCaseIds = EscalationCarryForwardCaseIds.Array(); + Summary.PinnedCarryForwardCaseIds = PinnedCarryForwardCaseIds.Array(); + Summary.SuppressedResolvedCaseIds = SuppressedResolvedCaseIds.Array(); + Summary.RecurringCarryForwardCaseIds.Sort(); + Summary.EscalationCarryForwardCaseIds.Sort(); + Summary.PinnedCarryForwardCaseIds.Sort(); + Summary.SuppressedResolvedCaseIds.Sort(); + Summary.RecurringCarryForwardCaseCount = Summary.RecurringCarryForwardCaseIds.Num(); + Summary.EscalationCarryForwardCaseCount = Summary.EscalationCarryForwardCaseIds.Num(); + Summary.PinnedCarryForwardCaseCount = Summary.PinnedCarryForwardCaseIds.Num(); + Summary.SuppressedResolvedCaseCount = Summary.SuppressedResolvedCaseIds.Num(); + + Summary.bSuppressResolvedResurfacing = Summary.SuppressedResolvedCaseCount > 0; + Summary.bPinRecurringCases = Summary.PinnedCarryForwardCaseCount > 0; + Summary.bNeedsEscalationHandOff = + ClosureMemory.bHasEscalationPressure || Summary.EscalationCarryForwardCaseCount > 0; + Summary.bBiasCoachReviewed = + Summary.bNeedsEscalationHandOff + || RecommendationHistory.bShouldBiasCoachReviewed + || (Summary.PinnedCarryForwardCaseCount > 0 + && Summary.DominantSuggestedMode == EHyperTwistTrainingDeliveryMode::CoachReviewed); + Summary.bHasActiveCarryForwardPressure = + Summary.bNeedsEscalationHandOff + || Summary.bPinRecurringCases + || RecommendationHistory.bHasRepeatRecommendationPressure + || ClosureMemory.bHasFollowUpPressure; + + Summary.CarryForwardIntensityScore = FMath::Clamp( + (static_cast(Summary.EscalationCarryForwardCaseCount) * 25.0f) + + (static_cast(Summary.RecurringCarryForwardCaseCount) * 12.0f) + + (static_cast(RecommendationHistory.RepeatRecommendationSequenceCount) * 8.0f) + + (ClosureMemory.bHasFollowUpPressure ? 10.0f : 0.0f) + - (static_cast(Summary.SuppressedResolvedCaseCount) * 2.0f), + 0.0f, + 100.0f + ); + + return Summary; +} + +FHyperTwistTrainingCoachArchivePolicySummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachArchivePolicySummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachArchivePolicySummary Summary; + if (UserId.IsEmpty()) + { + return Summary; + } + + const FHyperTwistTrainingCoachClosureMemorySummary ClosureMemory = + DeriveCoachClosureMemorySummary(RepositoryState, UserId, ReferenceUtc); + const FHyperTwistTrainingCoachRecommendationHistorySummary RecommendationHistory = + DeriveCoachRecommendationHistorySummary(RepositoryState, UserId, ReferenceUtc); + const FHyperTwistTrainingCoachCarryForwardPolicySummary CarryForwardPolicy = + DeriveCoachCarryForwardPolicySummary(RepositoryState, UserId, ReferenceUtc); + if (!ClosureMemory.IsStructurallyValid() + && !RecommendationHistory.IsStructurallyValid() + && !CarryForwardPolicy.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + Summary.FocusDeckId = CarryForwardPolicy.IsStructurallyValid() + ? CarryForwardPolicy.FocusDeckId + : (!RecommendationHistory.FocusDeckId.IsEmpty() ? RecommendationHistory.FocusDeckId : ClosureMemory.FocusDeckId); + Summary.FocusMethodSegmentId = CarryForwardPolicy.IsStructurallyValid() + ? CarryForwardPolicy.FocusMethodSegmentId + : (!RecommendationHistory.FocusMethodSegmentId.IsEmpty() + ? RecommendationHistory.FocusMethodSegmentId + : ClosureMemory.FocusMethodSegmentId); + Summary.DominantSuggestedMode = CarryForwardPolicy.IsStructurallyValid() + ? CarryForwardPolicy.DominantSuggestedMode + : RecommendationHistory.DominantSuggestedMode; + Summary.DominantSelectionPolicy = CarryForwardPolicy.IsStructurallyValid() + ? CarryForwardPolicy.DominantSelectionPolicy + : RecommendationHistory.DominantSelectionPolicy; + + const TArray UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId); + TMap LatestPlanByRootId; + TMap LatestSortUtcByRootId; + + for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans) + { + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan); + if (!NormalizedPlan.IsStructurallyValid()) + { + continue; + } + + const FString RootPlanId = !NormalizedPlan.RootPlanId.IsEmpty() + ? NormalizedPlan.RootPlanId + : NormalizedPlan.PlanId; + const FString SortUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() + ? NormalizedPlan.CompletedAtUtc + : (!NormalizedPlan.StartedAtUtc.IsEmpty() ? NormalizedPlan.StartedAtUtc : NormalizedPlan.CreatedAtUtc); + + FHyperTwistTrainingCoachActionPlan* ExistingPlan = LatestPlanByRootId.Find(RootPlanId); + FString* ExistingSortUtc = LatestSortUtcByRootId.Find(RootPlanId); + if (ExistingPlan == nullptr || ExistingSortUtc == nullptr) + { + LatestPlanByRootId.Add(RootPlanId, NormalizedPlan); + LatestSortUtcByRootId.Add(RootPlanId, SortUtc); + continue; + } + + const int32 CompareResult = + HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, *ExistingSortUtc); + if (CompareResult > 0 + || (CompareResult == 0 && NormalizedPlan.FollowUpDepth >= ExistingPlan->FollowUpDepth)) + { + *ExistingPlan = NormalizedPlan; + *ExistingSortUtc = SortUtc; + } + } + + TMap ResolvedArchiveHitCounts; + TMap OpenCarryForwardHitCounts; + TMap EscalationHitCounts; + TMap StableArchiveHitCounts; + const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot = + HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot( + RepositoryState, + UserId, + Summary.ReferenceUtc + ); + + for (const TPair& Pair : LatestPlanByRootId) + { + const FHyperTwistTrainingCoachActionClosureSummary ClosureSummary = + HyperTwistTrainingRepositoryLibraryInternal::DeriveCoachActionClosureSummaryWithCoachMemory( + RepositoryState, + Pair.Value, + Summary.ReferenceUtc, + CoachMemorySnapshot + ); + if (!ClosureSummary.IsStructurallyValid()) + { + continue; + } + + if (ClosureSummary.bShouldArchiveResolvedCases) + { + for (const FString& CaseId : ClosureSummary.ResolvedCaseIds) + { + if (CaseId.IsEmpty()) + { + continue; + } + + ResolvedArchiveHitCounts.FindOrAdd(CaseId) += 1; + if (ClosureSummary.bSequenceClosed + && !ClosureSummary.bNeedsEscalation + && !ClosureSummary.bShouldSpawnFollowUp) + { + StableArchiveHitCounts.FindOrAdd(CaseId) += 1; + } + } + } + + for (const FString& CaseId : ClosureSummary.PersistentWeakCaseIds) + { + if (!CaseId.IsEmpty()) + { + OpenCarryForwardHitCounts.FindOrAdd(CaseId) += 1; + if (ClosureSummary.bNeedsEscalation) + { + EscalationHitCounts.FindOrAdd(CaseId) += 1; + } + } + } + for (const FString& CaseId : ClosureSummary.UnresolvedCaseIds) + { + if (!CaseId.IsEmpty()) + { + OpenCarryForwardHitCounts.FindOrAdd(CaseId) += 1; + if (ClosureSummary.bNeedsEscalation) + { + EscalationHitCounts.FindOrAdd(CaseId) += 1; + } + } + } + } + + TSet RecurringCaseIds; + for (const FString& CaseId : RecommendationHistory.RecurringCaseIds) + { + if (!CaseId.IsEmpty()) + { + RecurringCaseIds.Add(CaseId); + } + } + + TSet LongTermArchivedCaseIds; + TSet ExpiredCarryForwardCaseIds; + TSet RetainedCarryForwardCaseIds; + TSet EscalationRetainedCaseIds; + TSet SuppressedResolvedCaseIds; + TSet StableLongTermArchivedCaseIds; + TSet ProvisionalArchivedCaseIds; + TSet ReopenRiskArchivedCaseIds; + + for (const FString& CaseId : CarryForwardPolicy.SuppressedResolvedCaseIds) + { + if (!CaseId.IsEmpty()) + { + LongTermArchivedCaseIds.Add(CaseId); + SuppressedResolvedCaseIds.Add(CaseId); + + const int32 OpenCarryForwardHits = OpenCarryForwardHitCounts.FindRef(CaseId); + const int32 StableArchiveHits = StableArchiveHitCounts.FindRef(CaseId); + const bool bRecurringCase = RecurringCaseIds.Contains(CaseId); + if (StableArchiveHits > 0 && OpenCarryForwardHits == 0 && !bRecurringCase) + { + StableLongTermArchivedCaseIds.Add(CaseId); + } + else + { + ProvisionalArchivedCaseIds.Add(CaseId); + if (bRecurringCase || OpenCarryForwardHits > 0) + { + ReopenRiskArchivedCaseIds.Add(CaseId); + } + } + } + } + + const bool bStableClosureMemory = + ClosureMemory.AverageClosureScore >= 70.0f + && !ClosureMemory.bHasEscalationPressure + && !ClosureMemory.bHasFollowUpPressure; + + for (const FString& CaseId : CarryForwardPolicy.PinnedCarryForwardCaseIds) + { + if (CaseId.IsEmpty()) + { + continue; + } + + const int32 ResolvedArchiveHits = ResolvedArchiveHitCounts.FindRef(CaseId); + const int32 OpenCarryForwardHits = OpenCarryForwardHitCounts.FindRef(CaseId); + const int32 EscalationHits = EscalationHitCounts.FindRef(CaseId); + const int32 StableArchiveHits = StableArchiveHitCounts.FindRef(CaseId); + const bool bRecurringCase = RecurringCaseIds.Contains(CaseId); + + const bool bCanExpireResolvedCarryForward = + EscalationHits == 0 + && ResolvedArchiveHits > 0 + && OpenCarryForwardHits <= ResolvedArchiveHits + && (StableArchiveHits > 0 + || bStableClosureMemory + || (ResolvedArchiveHits >= 2 && !bRecurringCase) + || (ResolvedArchiveHits > OpenCarryForwardHits + && RecommendationHistory.RepeatRecommendationSequenceCount <= 1)); + + if (bCanExpireResolvedCarryForward) + { + ExpiredCarryForwardCaseIds.Add(CaseId); + LongTermArchivedCaseIds.Add(CaseId); + if (StableArchiveHits > 0 && !bRecurringCase && OpenCarryForwardHits == 0) + { + StableLongTermArchivedCaseIds.Add(CaseId); + } + else + { + ProvisionalArchivedCaseIds.Add(CaseId); + if (bRecurringCase + || RecommendationHistory.bHasRepeatRecommendationPressure + || OpenCarryForwardHits > 0) + { + ReopenRiskArchivedCaseIds.Add(CaseId); + } + } + continue; + } + + RetainedCarryForwardCaseIds.Add(CaseId); + if (EscalationHits > 0) + { + EscalationRetainedCaseIds.Add(CaseId); + } + } + + Summary.LongTermArchivedCaseIds = LongTermArchivedCaseIds.Array(); + Summary.ExpiredCarryForwardCaseIds = ExpiredCarryForwardCaseIds.Array(); + Summary.RetainedCarryForwardCaseIds = RetainedCarryForwardCaseIds.Array(); + Summary.EscalationRetainedCaseIds = EscalationRetainedCaseIds.Array(); + Summary.SuppressedResolvedCaseIds = SuppressedResolvedCaseIds.Array(); + Summary.StableLongTermArchivedCaseIds = StableLongTermArchivedCaseIds.Array(); + Summary.ProvisionalArchivedCaseIds = ProvisionalArchivedCaseIds.Array(); + Summary.ReopenRiskArchivedCaseIds = ReopenRiskArchivedCaseIds.Array(); + Summary.LongTermArchivedCaseIds.Sort(); + Summary.ExpiredCarryForwardCaseIds.Sort(); + Summary.RetainedCarryForwardCaseIds.Sort(); + Summary.EscalationRetainedCaseIds.Sort(); + Summary.SuppressedResolvedCaseIds.Sort(); + Summary.StableLongTermArchivedCaseIds.Sort(); + Summary.ProvisionalArchivedCaseIds.Sort(); + Summary.ReopenRiskArchivedCaseIds.Sort(); + + Summary.LongTermArchivedCaseCount = Summary.LongTermArchivedCaseIds.Num(); + Summary.ExpiredCarryForwardCaseCount = Summary.ExpiredCarryForwardCaseIds.Num(); + Summary.RetainedCarryForwardCaseCount = Summary.RetainedCarryForwardCaseIds.Num(); + Summary.EscalationRetainedCaseCount = Summary.EscalationRetainedCaseIds.Num(); + Summary.SuppressedResolvedCaseCount = Summary.SuppressedResolvedCaseIds.Num(); + Summary.StableLongTermArchivedCaseCount = Summary.StableLongTermArchivedCaseIds.Num(); + Summary.ProvisionalArchivedCaseCount = Summary.ProvisionalArchivedCaseIds.Num(); + Summary.ReopenRiskArchivedCaseCount = Summary.ReopenRiskArchivedCaseIds.Num(); + + Summary.bShouldExpireResolvedCarryForward = Summary.ExpiredCarryForwardCaseCount > 0; + Summary.bShouldRetainEscalationCarryForward = Summary.EscalationRetainedCaseCount > 0; + Summary.ArchiveStabilityScore = FMath::Clamp( + ClosureMemory.AverageClosureScore + + (static_cast(Summary.LongTermArchivedCaseCount) * 5.0f) + + (static_cast(Summary.StableLongTermArchivedCaseCount) * 4.0f) + + (bStableClosureMemory ? 10.0f : 0.0f) + - (static_cast(Summary.ProvisionalArchivedCaseCount) * 3.0f) + - (static_cast(Summary.ReopenRiskArchivedCaseCount) * 8.0f) + - (static_cast(Summary.RetainedCarryForwardCaseCount) * 4.0f) + - (static_cast(Summary.EscalationRetainedCaseCount) * 10.0f) + - (RecommendationHistory.bHasRepeatRecommendationPressure ? 6.0f : 0.0f), + 0.0f, + 100.0f + ); + Summary.ArchiveRetentionTuningScore = FMath::Clamp( + (static_cast(Summary.ReopenRiskArchivedCaseCount) * 18.0f) + + (static_cast(Summary.ProvisionalArchivedCaseCount) * 9.0f) + + (RecommendationHistory.bHasRepeatRecommendationPressure ? 12.0f : 0.0f) + + (Summary.bShouldRetainEscalationCarryForward ? 14.0f : 0.0f) + - (static_cast(Summary.StableLongTermArchivedCaseCount) * 6.0f), + 0.0f, + 100.0f + ); + Summary.bHasStableArchiveMemory = + Summary.LongTermArchivedCaseCount > 0 + && Summary.ArchiveStabilityScore >= 55.0f + && Summary.EscalationRetainedCaseCount == 0; + Summary.bNeedsArchiveRetentionTuning = + Summary.ReopenRiskArchivedCaseCount > 0 + || Summary.ArchiveRetentionTuningScore >= 25.0f + || (Summary.ProvisionalArchivedCaseCount > Summary.StableLongTermArchivedCaseCount + && Summary.LongTermArchivedCaseCount > 0); + Summary.bHasActiveArchivePressure = + Summary.RetainedCarryForwardCaseCount > 0 + || Summary.EscalationRetainedCaseCount > 0 + || Summary.bNeedsArchiveRetentionTuning; + + return Summary; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h index 1193980..f47a899 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h @@ -12,6 +12,12 @@ namespace HyperTwistTrainingRepositoryLibraryInternal int32 ComputeElapsedMilliseconds(const FString& OlderUtc, const FString& NewerUtc); + FHyperTwistTrainingCoachMemorySnapshot DeriveQueueRecoveryBiasSnapshot( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc + ); + int32 ComputePlanDurationMs( const FHyperTwistTrainingReviewPlanState& ReviewPlan, const FString& ReferenceUtc @@ -69,4 +75,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry, const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry ); + + FHyperTwistTrainingCoachActionClosureSummary DeriveCoachActionClosureSummaryWithCoachMemory( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& AnchorPlan, + const FString& ReferenceUtc, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot + ); } diff --git a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md index fa83346..6dd81c9 100644 --- a/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md +++ b/docs/ops/HYPERTWIST_REFACTORING_TOOLCHAIN_2026-06-22.md @@ -1721,3 +1721,52 @@ Latest native repository coach-summary split follow-up later on `2026-06-29`: - the next worthwhile continuation remains another bounded training-repository or validation-library seam, not fresh product-family widening + +Latest native repository coach-closure/policy 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: + - moved the coach action-closure helper plus coach closure-memory, + carry-forward policy, and archive policy family out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - kept the much larger recommendation-history family in the main repository + unit on purpose so this packet stayed bounded instead of mixing two hotspot + seams at once + - the extracted implementation family now lives at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachPolicies.cpp` + - the existing private helper declaration seam in + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h` + was widened by two bounded declarations so the split policy family can + reuse only the queue-recovery bias snapshot and the shared closure helper + it actually needs + - the main repository file now stands at `29,532` lines and the extracted + coach-policy unit stands at `946` lines +- the HyperTwist-owned structural loop stayed healthy on that exact native + state: + - `scripts/run-hypertwist-sentrux-source-only.sh` + - `Quality: 6234` + - all `7` rules pass + - `scripts/run-hypertwist-sentrux-gate.sh` + - comparison result: + - `Quality: 6234 -> 6234` + - `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.96 seconds` +- current truthful reading after this native follow-up: + - the coach-closure and policy extraction is now build-proven on the + canonical Windows lane + - vanilla refactoring still is not fully complete because the training + repository library still holds the oversized recommendation-history hotspot + and the validation benchmark library remains another real native cleanup + target + - the next worthwhile continuation remains another bounded + training-repository or validation-library seam rather than fresh + product-topology or renderer widening diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md index 3ae67b7..2bc2669 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/DEVELOPMENT.md @@ -2544,3 +2544,52 @@ Latest same-family responsive public-route expansion follow-up on `2026-06-29`: - the next worthwhile continuation remains another bounded training-repository or validation-library seam instead of fresh product-topology widening + +## Latest native repository coach-closure/policy 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-closure helper plus coach closure-memory, + carry-forward policy, and archive policy family was moved out of + `HyperTwistTrainingRepositoryLibrary.cpp` + - the much larger recommendation-history family was intentionally kept in + the main repository unit so this packet stayed bounded instead of mixing + two hotspot seams at once + - the extracted implementation family now lives at + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachPolicies.cpp` + - the existing private helper declaration seam in + `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h` + was widened by two bounded declarations so the split policy family can + reuse only the queue-recovery bias snapshot and the shared closure helper + it actually needs + - the main repository file now stands at `29,532` lines and the extracted + coach-policy unit stands at `946` lines +- the HyperTwist-owned structural loop stayed healthy on that exact native + state: + - `scripts/run-hypertwist-sentrux-source-only.sh` + - `Quality: 6234` + - all `7` rules pass + - `scripts/run-hypertwist-sentrux-gate.sh` + - comparison result: + - `Quality: 6234 -> 6234` + - `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.96 seconds` +- current truthful reading after this native follow-up: + - the coach-closure and policy extraction is now build-proven on the + canonical Windows lane + - vanilla refactoring still is not fully complete because the training + repository library still holds the oversized recommendation-history hotspot + and the validation benchmark library remains another real native cleanup + target + - the next worthwhile continuation remains another bounded + training-repository or validation-library seam instead of fresh + product-topology widening