Split HyperTwist coach closure policy repository family

This commit is contained in:
axiomlogicnexus 2026-06-30 00:09:14 +00:00
parent 677fbb5dbb
commit ae625a8891
5 changed files with 1057 additions and 939 deletions

View file

@ -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<FHyperTwistTrainingCoachActionPlan> SequencePlans;
const TArray<FHyperTwistTrainingCoachActionPlan> 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<FString> AllFocusCaseIds;
TSet<FString> ResolvedCaseIds;
TSet<FString> UnresolvedCaseIds;
TMap<FString, int32> CaseAttemptCounts;
TMap<FString, int32> CaseNonSuccessCounts;
TMap<FString, int32> CaseSuccessCounts;
bool bAnyRecognition = false;
for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans)
{
const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary =
UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary(
RepositoryState,
Plan,
SequenceSummary.ReferenceUtc
);
TSet<FString> 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<FString> 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<FString> PersistentWeakCaseIds;
for (const TPair<FString, int32>& 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<float>(Summary.ResolvedCaseCount) * 6.0f)
- (static_cast<float>(Summary.PersistentWeakCaseCount) * 8.0f)
- (static_cast<float>(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<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId);
TMap<FString, FHyperTwistTrainingCoachActionPlan> LatestPlanByRootId;
TMap<FString, FString> 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<FString> ArchivedResolvedCaseIds;
TSet<FString> PersistentWeakCaseIds;
TSet<FString> EscalationCaseIds;
const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot =
HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot(
RepositoryState,
UserId,
Summary.ReferenceUtc
);
for (const TPair<FString, FHyperTwistTrainingCoachActionPlan>& 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<float>(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<FString> RecurringCarryForwardCaseIds;
TSet<FString> EscalationCarryForwardCaseIds;
TSet<FString> PinnedCarryForwardCaseIds;
TSet<FString> 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<float>(Summary.EscalationCarryForwardCaseCount) * 25.0f)
+ (static_cast<float>(Summary.RecurringCarryForwardCaseCount) * 12.0f)
+ (static_cast<float>(RecommendationHistory.RepeatRecommendationSequenceCount) * 8.0f)
+ (ClosureMemory.bHasFollowUpPressure ? 10.0f : 0.0f)
- (static_cast<float>(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<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId);
TMap<FString, FHyperTwistTrainingCoachActionPlan> LatestPlanByRootId;
TMap<FString, FString> 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<FString, int32> ResolvedArchiveHitCounts;
TMap<FString, int32> OpenCarryForwardHitCounts;
TMap<FString, int32> EscalationHitCounts;
TMap<FString, int32> StableArchiveHitCounts;
const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot =
HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot(
RepositoryState,
UserId,
Summary.ReferenceUtc
);
for (const TPair<FString, FHyperTwistTrainingCoachActionPlan>& 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<FString> RecurringCaseIds;
for (const FString& CaseId : RecommendationHistory.RecurringCaseIds)
{
if (!CaseId.IsEmpty())
{
RecurringCaseIds.Add(CaseId);
}
}
TSet<FString> LongTermArchivedCaseIds;
TSet<FString> ExpiredCarryForwardCaseIds;
TSet<FString> RetainedCarryForwardCaseIds;
TSet<FString> EscalationRetainedCaseIds;
TSet<FString> SuppressedResolvedCaseIds;
TSet<FString> StableLongTermArchivedCaseIds;
TSet<FString> ProvisionalArchivedCaseIds;
TSet<FString> 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<float>(Summary.LongTermArchivedCaseCount) * 5.0f)
+ (static_cast<float>(Summary.StableLongTermArchivedCaseCount) * 4.0f)
+ (bStableClosureMemory ? 10.0f : 0.0f)
- (static_cast<float>(Summary.ProvisionalArchivedCaseCount) * 3.0f)
- (static_cast<float>(Summary.ReopenRiskArchivedCaseCount) * 8.0f)
- (static_cast<float>(Summary.RetainedCarryForwardCaseCount) * 4.0f)
- (static_cast<float>(Summary.EscalationRetainedCaseCount) * 10.0f)
- (RecommendationHistory.bHasRepeatRecommendationPressure ? 6.0f : 0.0f),
0.0f,
100.0f
);
Summary.ArchiveRetentionTuningScore = FMath::Clamp(
(static_cast<float>(Summary.ReopenRiskArchivedCaseCount) * 18.0f)
+ (static_cast<float>(Summary.ProvisionalArchivedCaseCount) * 9.0f)
+ (RecommendationHistory.bHasRepeatRecommendationPressure ? 12.0f : 0.0f)
+ (Summary.bShouldRetainEscalationCarryForward ? 14.0f : 0.0f)
- (static_cast<float>(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,

View file

@ -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<FHyperTwistTrainingCoachActionPlan> SequencePlans;
const TArray<FHyperTwistTrainingCoachActionPlan> 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<FString> AllFocusCaseIds;
TSet<FString> ResolvedCaseIds;
TSet<FString> UnresolvedCaseIds;
TMap<FString, int32> CaseAttemptCounts;
TMap<FString, int32> CaseNonSuccessCounts;
TMap<FString, int32> CaseSuccessCounts;
bool bAnyRecognition = false;
for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans)
{
const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary =
UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary(
RepositoryState,
Plan,
SequenceSummary.ReferenceUtc
);
TSet<FString> 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<FString> 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<FString> PersistentWeakCaseIds;
for (const TPair<FString, int32>& 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<float>(Summary.ResolvedCaseCount) * 6.0f)
- (static_cast<float>(Summary.PersistentWeakCaseCount) * 8.0f)
- (static_cast<float>(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<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId);
TMap<FString, FHyperTwistTrainingCoachActionPlan> LatestPlanByRootId;
TMap<FString, FString> 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<FString> ArchivedResolvedCaseIds;
TSet<FString> PersistentWeakCaseIds;
TSet<FString> EscalationCaseIds;
const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot =
HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot(
RepositoryState,
UserId,
Summary.ReferenceUtc
);
for (const TPair<FString, FHyperTwistTrainingCoachActionPlan>& 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<float>(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<FString> RecurringCarryForwardCaseIds;
TSet<FString> EscalationCarryForwardCaseIds;
TSet<FString> PinnedCarryForwardCaseIds;
TSet<FString> 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<float>(Summary.EscalationCarryForwardCaseCount) * 25.0f)
+ (static_cast<float>(Summary.RecurringCarryForwardCaseCount) * 12.0f)
+ (static_cast<float>(RecommendationHistory.RepeatRecommendationSequenceCount) * 8.0f)
+ (ClosureMemory.bHasFollowUpPressure ? 10.0f : 0.0f)
- (static_cast<float>(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<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(RepositoryState, UserId);
TMap<FString, FHyperTwistTrainingCoachActionPlan> LatestPlanByRootId;
TMap<FString, FString> 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<FString, int32> ResolvedArchiveHitCounts;
TMap<FString, int32> OpenCarryForwardHitCounts;
TMap<FString, int32> EscalationHitCounts;
TMap<FString, int32> StableArchiveHitCounts;
const FHyperTwistTrainingCoachMemorySnapshot CoachMemorySnapshot =
HyperTwistTrainingRepositoryLibraryInternal::DeriveQueueRecoveryBiasSnapshot(
RepositoryState,
UserId,
Summary.ReferenceUtc
);
for (const TPair<FString, FHyperTwistTrainingCoachActionPlan>& 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<FString> RecurringCaseIds;
for (const FString& CaseId : RecommendationHistory.RecurringCaseIds)
{
if (!CaseId.IsEmpty())
{
RecurringCaseIds.Add(CaseId);
}
}
TSet<FString> LongTermArchivedCaseIds;
TSet<FString> ExpiredCarryForwardCaseIds;
TSet<FString> RetainedCarryForwardCaseIds;
TSet<FString> EscalationRetainedCaseIds;
TSet<FString> SuppressedResolvedCaseIds;
TSet<FString> StableLongTermArchivedCaseIds;
TSet<FString> ProvisionalArchivedCaseIds;
TSet<FString> 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<float>(Summary.LongTermArchivedCaseCount) * 5.0f)
+ (static_cast<float>(Summary.StableLongTermArchivedCaseCount) * 4.0f)
+ (bStableClosureMemory ? 10.0f : 0.0f)
- (static_cast<float>(Summary.ProvisionalArchivedCaseCount) * 3.0f)
- (static_cast<float>(Summary.ReopenRiskArchivedCaseCount) * 8.0f)
- (static_cast<float>(Summary.RetainedCarryForwardCaseCount) * 4.0f)
- (static_cast<float>(Summary.EscalationRetainedCaseCount) * 10.0f)
- (RecommendationHistory.bHasRepeatRecommendationPressure ? 6.0f : 0.0f),
0.0f,
100.0f
);
Summary.ArchiveRetentionTuningScore = FMath::Clamp(
(static_cast<float>(Summary.ReopenRiskArchivedCaseCount) * 18.0f)
+ (static_cast<float>(Summary.ProvisionalArchivedCaseCount) * 9.0f)
+ (RecommendationHistory.bHasRepeatRecommendationPressure ? 12.0f : 0.0f)
+ (Summary.bShouldRetainEscalationCarryForward ? 14.0f : 0.0f)
- (static_cast<float>(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;
}

View file

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

View file

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

View file

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