Split HyperTwist coach action summary repository family

This commit is contained in:
axiomlogicnexus 2026-06-29 23:54:43 +00:00
parent c6a88aa9eb
commit 677fbb5dbb
5 changed files with 712 additions and 618 deletions

View file

@ -24559,624 +24559,6 @@ FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingReposito
return Summary;
}
FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionPlanSummary Summary;
if (UserId.IsEmpty())
{
return Summary;
}
Summary.UserId = UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
const TArray<FHyperTwistTrainingCoachActionPlan> ActionPlans = ListCoachActionPlansForUser(
RepositoryState,
UserId
);
if (ActionPlans.Num() == 0)
{
return Summary;
}
Summary.TotalPlanCount = ActionPlans.Num();
int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0};
int32 HandoffKindCounts[4] = {0, 0, 0, 0};
TMap<FString, int32> FocusLaneCounts;
TMap<FString, int32> ActionLabelCounts;
TMap<FString, int32> HandoffSourceCounts;
int32 CompletedPlanDurationAccumulator = 0;
int32 CompletedPlanDurationCount = 0;
float PriorityScoreAccumulator = 0.0f;
FString LatestOpenPlanUtc;
FString LatestCompletedUtc;
for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : ActionPlans)
{
PriorityScoreAccumulator += ActionPlan.PriorityScore;
Summary.TotalEntryCount += ActionPlan.Entries.Num();
FocusLaneCounts.FindOrAdd(ActionPlan.FocusLaneLabel) += 1;
ActionLabelCounts.FindOrAdd(ActionPlan.PrimaryActionLabel) += 1;
const int32 SuggestedModeIndex = static_cast<int32>(ActionPlan.SuggestedMode);
if (SuggestedModeIndex >= 0 && SuggestedModeIndex < UE_ARRAY_COUNT(SuggestedModeCounts))
{
SuggestedModeCounts[SuggestedModeIndex] += 1;
}
const int32 HandoffKindIndex = static_cast<int32>(ActionPlan.PreferredCoachHandoffKind);
if (HandoffKindIndex >= 0 && HandoffKindIndex < UE_ARRAY_COUNT(HandoffKindCounts))
{
HandoffKindCounts[HandoffKindIndex] += 1;
}
if (!ActionPlan.PreferredCoachHandoffSourceLabel.IsEmpty())
{
HandoffSourceCounts.FindOrAdd(ActionPlan.PreferredCoachHandoffSourceLabel) += 1;
}
int32 LocalCompletedEntries = 0;
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries)
{
if (Entry.bCompleted)
{
LocalCompletedEntries += 1;
}
if (Entry.bPreferRecognitionReplayReview)
{
Summary.bHasRecognitionBiasedPlans = true;
}
}
Summary.CompletedEntryCount += LocalCompletedEntries;
Summary.OpenEntryCount += ActionPlan.Entries.Num() - LocalCompletedEntries;
Summary.bHasRecognitionBiasedPlans = Summary.bHasRecognitionBiasedPlans || ActionPlan.bPreferRecognitionReplayReview;
switch (ActionPlan.PlanState)
{
case EHyperTwistTrainingCoachActionPlanState::Draft:
Summary.DraftPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Active:
Summary.ActivePlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Completed:
Summary.CompletedPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Aborted:
Summary.AbortedPlanCount += 1;
break;
default:
break;
}
if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Active
|| ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Draft)
{
const FString SortUtc = !ActionPlan.StartedAtUtc.IsEmpty()
? ActionPlan.StartedAtUtc
: ActionPlan.CreatedAtUtc;
if (Summary.CurrentPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestOpenPlanUtc) > 0)
{
Summary.CurrentPlanId = ActionPlan.PlanId;
Summary.FocusDeckId = ActionPlan.FocusDeckId;
Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId;
LatestOpenPlanUtc = SortUtc;
}
}
if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed
&& !ActionPlan.CompletedAtUtc.IsEmpty())
{
const int32 DurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds(
ActionPlan.CreatedAtUtc,
ActionPlan.CompletedAtUtc
);
if (DurationMs > 0)
{
CompletedPlanDurationAccumulator += DurationMs;
CompletedPlanDurationCount += 1;
if (Summary.BestCompletedPlanDurationMs == 0 || DurationMs < Summary.BestCompletedPlanDurationMs)
{
Summary.BestCompletedPlanDurationMs = DurationMs;
}
}
if (Summary.LastCompletedPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ActionPlan.CompletedAtUtc, LatestCompletedUtc) > 0)
{
Summary.LastCompletedPlanId = ActionPlan.PlanId;
LatestCompletedUtc = ActionPlan.CompletedAtUtc;
if (Summary.CurrentPlanId.IsEmpty())
{
Summary.FocusDeckId = ActionPlan.FocusDeckId;
Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId;
}
}
}
if (ActionPlan.bNeedsCoachReview
|| ActionPlan.bNeedsCadenceRecovery
|| ActionPlan.bNeedsVarietyRotation
|| ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted)
{
Summary.bNeedsCoachIntervention = true;
}
}
Summary.AveragePriorityScore = PriorityScoreAccumulator / static_cast<float>(Summary.TotalPlanCount);
if (CompletedPlanDurationCount > 0)
{
Summary.AverageCompletedPlanDurationMs =
CompletedPlanDurationAccumulator / CompletedPlanDurationCount;
}
int32 BestModeCount = 0;
for (int32 Index = 0; Index < UE_ARRAY_COUNT(SuggestedModeCounts); ++Index)
{
if (SuggestedModeCounts[Index] > BestModeCount)
{
BestModeCount = SuggestedModeCounts[Index];
Summary.DominantSuggestedMode = static_cast<EHyperTwistTrainingDeliveryMode>(Index);
}
}
int32 BestHandoffKindCount = 0;
for (int32 Index = 0; Index < UE_ARRAY_COUNT(HandoffKindCounts); ++Index)
{
if (HandoffKindCounts[Index] > BestHandoffKindCount)
{
BestHandoffKindCount = HandoffKindCounts[Index];
Summary.DominantPreferredCoachHandoffKind =
static_cast<EHyperTwistTrainingCoachHandoffKind>(Index);
}
}
int32 BestFocusLaneCount = 0;
for (const TPair<FString, int32>& Pair : FocusLaneCounts)
{
if (Pair.Value > BestFocusLaneCount)
{
BestFocusLaneCount = Pair.Value;
Summary.DominantFocusLaneLabel = Pair.Key;
}
}
int32 BestActionLabelCount = 0;
for (const TPair<FString, int32>& Pair : ActionLabelCounts)
{
if (Pair.Value > BestActionLabelCount)
{
BestActionLabelCount = Pair.Value;
Summary.DominantActionLabel = Pair.Key;
}
}
int32 BestHandoffSourceCount = 0;
for (const TPair<FString, int32>& Pair : HandoffSourceCounts)
{
if (Pair.Value > BestHandoffSourceCount)
{
BestHandoffSourceCount = Pair.Value;
Summary.DominantPreferredCoachHandoffSourceLabel = Pair.Key;
}
}
Summary.ActionPressureScore =
static_cast<float>(Summary.ActivePlanCount * 3)
+ static_cast<float>(Summary.DraftPlanCount * 2)
+ static_cast<float>(Summary.AbortedPlanCount * 2)
+ (static_cast<float>(Summary.OpenEntryCount) * 0.5f)
+ (Summary.bNeedsCoachIntervention ? 5.0f : 0.0f);
return Summary;
}
FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachActionPlan& ActionPlan,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionPlanOutcomeSummary Summary;
const FHyperTwistTrainingCoachActionPlan NormalizedPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan);
if (!NormalizedPlan.IsStructurallyValid())
{
return Summary;
}
Summary.UserId = NormalizedPlan.UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
!ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedPlan.ReferenceUtc
);
Summary.PlanId = NormalizedPlan.PlanId;
Summary.SourceSessionId = NormalizedPlan.SourceSessionId;
Summary.FocusDeckId = NormalizedPlan.FocusDeckId;
Summary.FocusMethodSegmentId = NormalizedPlan.FocusMethodSegmentId;
Summary.PrimaryActionLabel = NormalizedPlan.PrimaryActionLabel;
Summary.PlanState = NormalizedPlan.PlanState;
Summary.SuggestedMode = NormalizedPlan.SuggestedMode;
Summary.PreferredCoachHandoffKind = NormalizedPlan.PreferredCoachHandoffKind;
Summary.PreferredCoachHandoffSourceLabel = NormalizedPlan.PreferredCoachHandoffSourceLabel;
Summary.RequestedLaunchCaseCount = NormalizedPlan.RequestedLaunchCaseCount;
Summary.PlannedLaunchCaseCount = NormalizedPlan.PlannedLaunchCaseCount;
Summary.bBroadenedLaunchBudget = NormalizedPlan.bBroadenedLaunchBudget;
Summary.bTightenedLaunchBudget = NormalizedPlan.bTightenedLaunchBudget;
Summary.LaunchBudgetReasonLine = NormalizedPlan.LaunchBudgetReasonLine;
Summary.DeferredLaunchCaseIds = NormalizedPlan.DeferredLaunchCaseIds;
TSet<FString> FocusCaseIds;
for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty())
{
FocusCaseIds.Add(FocusCaseId);
}
}
TSet<FString> CompletedCaseIds;
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries)
{
if (!Entry.CaseId.IsEmpty() && FocusCaseIds.Num() == 0)
{
FocusCaseIds.Add(Entry.CaseId);
}
if (Entry.bCompleted && !Entry.CaseId.IsEmpty())
{
CompletedCaseIds.Add(Entry.CaseId);
}
}
Summary.FocusCaseCount = FocusCaseIds.Num();
FHyperTwistTrainingRunRecord RunRecord;
if (!NormalizedPlan.SourceSessionId.IsEmpty()
&& TryGetRunRecord(RepositoryState, NormalizedPlan.SourceSessionId, RunRecord))
{
TSet<FString> TouchedCaseIds;
int32 TotalTimeAccumulator = 0;
int32 TotalTimeSampleCount = 0;
int32 RecognitionTimeAccumulator = 0;
int32 RecognitionTimeSampleCount = 0;
for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory)
{
if (FocusCaseIds.Num() > 0 && !FocusCaseIds.Contains(AttemptHistoryEntry.CaseId))
{
continue;
}
Summary.AttemptCount += 1;
TouchedCaseIds.Add(AttemptHistoryEntry.CaseId);
if (!AttemptHistoryEntry.ReplayId.IsEmpty())
{
Summary.ReplayBackedAttemptCount += 1;
}
if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted
|| AttemptHistoryEntry.RecognitionTimeMs > 0)
{
Summary.RecognitionAttemptCount += 1;
}
if (AttemptHistoryEntry.TotalTimeMs > 0)
{
TotalTimeAccumulator += AttemptHistoryEntry.TotalTimeMs;
TotalTimeSampleCount += 1;
if (Summary.BestTotalTimeMs == 0 || AttemptHistoryEntry.TotalTimeMs < Summary.BestTotalTimeMs)
{
Summary.BestTotalTimeMs = AttemptHistoryEntry.TotalTimeMs;
}
}
if (AttemptHistoryEntry.RecognitionTimeMs > 0)
{
RecognitionTimeAccumulator += AttemptHistoryEntry.RecognitionTimeMs;
RecognitionTimeSampleCount += 1;
}
switch (AttemptHistoryEntry.Result)
{
case EHyperTwistTrainingAttemptResult::Success:
Summary.SuccessCount += 1;
if (!AttemptHistoryEntry.CaseId.IsEmpty())
{
CompletedCaseIds.Add(AttemptHistoryEntry.CaseId);
}
break;
case EHyperTwistTrainingAttemptResult::Failure:
Summary.FailureCount += 1;
break;
case EHyperTwistTrainingAttemptResult::Timeout:
Summary.TimeoutCount += 1;
break;
case EHyperTwistTrainingAttemptResult::DNF:
Summary.DNFCount += 1;
break;
case EHyperTwistTrainingAttemptResult::Aborted:
Summary.AbortedCount += 1;
break;
default:
break;
}
}
Summary.TouchedFocusCaseCount = TouchedCaseIds.Num();
if (TotalTimeSampleCount > 0)
{
Summary.AverageTotalTimeMs = TotalTimeAccumulator / TotalTimeSampleCount;
}
if (RecognitionTimeSampleCount > 0)
{
Summary.AverageRecognitionTimeMs = RecognitionTimeAccumulator / RecognitionTimeSampleCount;
}
}
Summary.CompletedFocusCaseCount = CompletedCaseIds.Num();
Summary.UntouchedFocusCaseCount = FMath::Max(0, Summary.FocusCaseCount - Summary.TouchedFocusCaseCount);
for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty() && !CompletedCaseIds.Contains(FocusCaseId))
{
Summary.FollowUpCaseIds.Add(FocusCaseId);
}
}
if (Summary.FollowUpCaseIds.Num() == 0)
{
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries)
{
if (!Entry.CaseId.IsEmpty()
&& !CompletedCaseIds.Contains(Entry.CaseId)
&& !Summary.FollowUpCaseIds.Contains(Entry.CaseId))
{
Summary.FollowUpCaseIds.Add(Entry.CaseId);
}
}
}
if (Summary.FocusCaseCount > 0)
{
Summary.CompletionRate = static_cast<float>(Summary.CompletedFocusCaseCount)
/ static_cast<float>(Summary.FocusCaseCount);
}
const FString PlanStartUtc = !NormalizedPlan.StartedAtUtc.IsEmpty()
? NormalizedPlan.StartedAtUtc
: NormalizedPlan.CreatedAtUtc;
const FString PlanEndUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty()
? NormalizedPlan.CompletedAtUtc
: Summary.ReferenceUtc;
if (!PlanStartUtc.IsEmpty() && !PlanEndUtc.IsEmpty())
{
Summary.PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds(
PlanStartUtc,
PlanEndUtc
);
}
const bool bPlanTerminal =
NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed
|| NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted;
Summary.bMetPrimaryGoal =
bPlanTerminal
&& NormalizedPlan.PlanState != EHyperTwistTrainingCoachActionPlanState::Aborted
&& (Summary.FocusCaseCount == 0
? Summary.SuccessCount > 0
: Summary.CompletedFocusCaseCount >= Summary.FocusCaseCount);
Summary.OutcomeScore =
(Summary.CompletionRate * 100.0f)
+ (static_cast<float>(Summary.SuccessCount) * 4.0f)
- (static_cast<float>(Summary.FailureCount) * 2.0f)
- (static_cast<float>(Summary.TimeoutCount) * 3.0f)
- (static_cast<float>(Summary.DNFCount) * 4.0f)
- (static_cast<float>(Summary.AbortedCount) * 5.0f)
- (static_cast<float>(Summary.UntouchedFocusCaseCount) * 4.0f)
+ (static_cast<float>(Summary.ReplayBackedAttemptCount) * 0.5f);
Summary.bNeedsFollowUpPlan =
Summary.FollowUpCaseIds.Num() > 0
|| Summary.DeferredLaunchCaseIds.Num() > 0
|| (
bPlanTerminal
&& (
NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted
|| !Summary.bMetPrimaryGoal
|| Summary.FailureCount > 0
|| Summary.TimeoutCount > 0
|| Summary.DNFCount > 0
|| (NormalizedPlan.bNeedsCoachReview && Summary.ReplayBackedAttemptCount == 0)
));
return Summary;
}
FHyperTwistTrainingCoachActionSequenceSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachActionPlan& AnchorPlan,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionSequenceSummary Summary;
const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan);
if (!NormalizedAnchorPlan.IsStructurallyValid())
{
return Summary;
}
const FString RootPlanId = !NormalizedAnchorPlan.RootPlanId.IsEmpty()
? NormalizedAnchorPlan.RootPlanId
: NormalizedAnchorPlan.PlanId;
Summary.UserId = NormalizedAnchorPlan.UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
!ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedAnchorPlan.ReferenceUtc
);
Summary.RootPlanId = RootPlanId;
const TArray<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(
RepositoryState,
NormalizedAnchorPlan.UserId
);
TArray<FHyperTwistTrainingCoachActionPlan> SequencePlans;
for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans)
{
const FHyperTwistTrainingCoachActionPlan NormalizedPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan);
const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty()
? NormalizedPlan.RootPlanId
: NormalizedPlan.PlanId;
if (CandidateRootPlanId == RootPlanId
|| NormalizedPlan.PlanId == RootPlanId
|| NormalizedPlan.ParentPlanId == RootPlanId)
{
SequencePlans.Add(NormalizedPlan);
}
}
if (SequencePlans.Num() == 0)
{
SequencePlans.Add(NormalizedAnchorPlan);
}
const auto ResolvePlanSortUtc = [](const FHyperTwistTrainingCoachActionPlan& Plan) -> FString
{
return !Plan.CompletedAtUtc.IsEmpty()
? Plan.CompletedAtUtc
: (!Plan.StartedAtUtc.IsEmpty() ? Plan.StartedAtUtc : Plan.CreatedAtUtc);
};
SequencePlans.Sort(
[&ResolvePlanSortUtc](const FHyperTwistTrainingCoachActionPlan& Left,
const FHyperTwistTrainingCoachActionPlan& Right)
{
const FString LeftSortUtc = ResolvePlanSortUtc(Left);
const FString RightSortUtc = ResolvePlanSortUtc(Right);
const int32 CompareResult =
HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(LeftSortUtc, RightSortUtc);
if (CompareResult != 0)
{
return CompareResult < 0;
}
if (Left.FollowUpDepth != Right.FollowUpDepth)
{
return Left.FollowUpDepth < Right.FollowUpDepth;
}
return Left.PlanId < Right.PlanId;
}
);
FString LatestSortUtc;
TSet<FString> OutstandingFollowUpCases;
float CompletionRateAccumulator = 0.0f;
float OutcomeScoreAccumulator = 0.0f;
bool bHasGenericFollowUpPressure = false;
for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans)
{
Summary.PlanCount += 1;
Summary.CarryForwardPlanCount += Plan.FollowUpDepth > 0 ? 1 : 0;
Summary.MaxFollowUpDepth = FMath::Max(Summary.MaxFollowUpDepth, Plan.FollowUpDepth);
switch (Plan.PlanState)
{
case EHyperTwistTrainingCoachActionPlanState::Active:
Summary.ActivePlanCount += 1;
Summary.bHasOpenPlan = true;
break;
case EHyperTwistTrainingCoachActionPlanState::Completed:
Summary.CompletedPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Aborted:
Summary.AbortedPlanCount += 1;
break;
default:
Summary.bHasOpenPlan = true;
break;
}
const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary =
DeriveCoachActionPlanOutcomeSummary(RepositoryState, Plan, Summary.ReferenceUtc);
Summary.TotalFocusCaseCount +=
OutcomeSummary.FocusCaseCount + OutcomeSummary.DeferredLaunchCaseIds.Num();
CompletionRateAccumulator += OutcomeSummary.CompletionRate;
OutcomeScoreAccumulator += OutcomeSummary.OutcomeScore;
TSet<FString> PlanFocusCaseIds;
for (const FString& FocusCaseId : Plan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty())
{
PlanFocusCaseIds.Add(FocusCaseId);
}
}
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries)
{
if (!Entry.CaseId.IsEmpty())
{
PlanFocusCaseIds.Add(Entry.CaseId);
}
}
TSet<FString> PlanOutstandingCaseIds;
for (const FString& CaseId : OutcomeSummary.FollowUpCaseIds)
{
if (!CaseId.IsEmpty())
{
PlanOutstandingCaseIds.Add(CaseId);
OutstandingFollowUpCases.Add(CaseId);
}
}
for (const FString& CaseId : OutcomeSummary.DeferredLaunchCaseIds)
{
if (!CaseId.IsEmpty())
{
PlanOutstandingCaseIds.Add(CaseId);
OutstandingFollowUpCases.Add(CaseId);
}
}
for (const FString& FocusCaseId : PlanFocusCaseIds)
{
if (!PlanOutstandingCaseIds.Contains(FocusCaseId))
{
OutstandingFollowUpCases.Remove(FocusCaseId);
}
}
bHasGenericFollowUpPressure =
OutcomeSummary.bNeedsFollowUpPlan && PlanOutstandingCaseIds.Num() == 0;
const FString SortUtc = ResolvePlanSortUtc(Plan);
if (Summary.LatestPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) > 0
|| (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) == 0
&& Plan.FollowUpDepth >= Summary.MaxFollowUpDepth))
{
Summary.LatestPlanId = Plan.PlanId;
Summary.FocusDeckId = Plan.FocusDeckId;
Summary.FocusMethodSegmentId = Plan.FocusMethodSegmentId;
Summary.LatestOutcomeScore = OutcomeSummary.OutcomeScore;
LatestSortUtc = SortUtc;
}
}
if (Summary.PlanCount > 0)
{
Summary.AverageCompletionRate = CompletionRateAccumulator / static_cast<float>(Summary.PlanCount);
Summary.AverageOutcomeScore = OutcomeScoreAccumulator / static_cast<float>(Summary.PlanCount);
}
Summary.UnresolvedFollowUpCaseCount = OutstandingFollowUpCases.Num();
Summary.bNeedsMoreFollowUp =
Summary.UnresolvedFollowUpCaseCount > 0 || bHasGenericFollowUpPressure || Summary.bHasOpenPlan;
return Summary;
}
namespace HyperTwistTrainingRepositoryLibraryInternal
{
FHyperTwistTrainingCoachActionClosureSummary DeriveCoachActionClosureSummaryWithCoachMemory(

View file

@ -0,0 +1,625 @@
// Copyright HyperTwist, Inc. All Rights Reserved.
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HyperTwistTrainingRepositoryLibraryInternal.h"
FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionPlanSummary Summary;
if (UserId.IsEmpty())
{
return Summary;
}
Summary.UserId = UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
const TArray<FHyperTwistTrainingCoachActionPlan> ActionPlans = ListCoachActionPlansForUser(
RepositoryState,
UserId
);
if (ActionPlans.Num() == 0)
{
return Summary;
}
Summary.TotalPlanCount = ActionPlans.Num();
int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0};
int32 HandoffKindCounts[4] = {0, 0, 0, 0};
TMap<FString, int32> FocusLaneCounts;
TMap<FString, int32> ActionLabelCounts;
TMap<FString, int32> HandoffSourceCounts;
int32 CompletedPlanDurationAccumulator = 0;
int32 CompletedPlanDurationCount = 0;
float PriorityScoreAccumulator = 0.0f;
FString LatestOpenPlanUtc;
FString LatestCompletedUtc;
for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : ActionPlans)
{
PriorityScoreAccumulator += ActionPlan.PriorityScore;
Summary.TotalEntryCount += ActionPlan.Entries.Num();
FocusLaneCounts.FindOrAdd(ActionPlan.FocusLaneLabel) += 1;
ActionLabelCounts.FindOrAdd(ActionPlan.PrimaryActionLabel) += 1;
const int32 SuggestedModeIndex = static_cast<int32>(ActionPlan.SuggestedMode);
if (SuggestedModeIndex >= 0 && SuggestedModeIndex < UE_ARRAY_COUNT(SuggestedModeCounts))
{
SuggestedModeCounts[SuggestedModeIndex] += 1;
}
const int32 HandoffKindIndex = static_cast<int32>(ActionPlan.PreferredCoachHandoffKind);
if (HandoffKindIndex >= 0 && HandoffKindIndex < UE_ARRAY_COUNT(HandoffKindCounts))
{
HandoffKindCounts[HandoffKindIndex] += 1;
}
if (!ActionPlan.PreferredCoachHandoffSourceLabel.IsEmpty())
{
HandoffSourceCounts.FindOrAdd(ActionPlan.PreferredCoachHandoffSourceLabel) += 1;
}
int32 LocalCompletedEntries = 0;
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries)
{
if (Entry.bCompleted)
{
LocalCompletedEntries += 1;
}
if (Entry.bPreferRecognitionReplayReview)
{
Summary.bHasRecognitionBiasedPlans = true;
}
}
Summary.CompletedEntryCount += LocalCompletedEntries;
Summary.OpenEntryCount += ActionPlan.Entries.Num() - LocalCompletedEntries;
Summary.bHasRecognitionBiasedPlans =
Summary.bHasRecognitionBiasedPlans || ActionPlan.bPreferRecognitionReplayReview;
switch (ActionPlan.PlanState)
{
case EHyperTwistTrainingCoachActionPlanState::Draft:
Summary.DraftPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Active:
Summary.ActivePlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Completed:
Summary.CompletedPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Aborted:
Summary.AbortedPlanCount += 1;
break;
default:
break;
}
if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Active
|| ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Draft)
{
const FString SortUtc = !ActionPlan.StartedAtUtc.IsEmpty()
? ActionPlan.StartedAtUtc
: ActionPlan.CreatedAtUtc;
if (Summary.CurrentPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestOpenPlanUtc) > 0)
{
Summary.CurrentPlanId = ActionPlan.PlanId;
Summary.FocusDeckId = ActionPlan.FocusDeckId;
Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId;
LatestOpenPlanUtc = SortUtc;
}
}
if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed
&& !ActionPlan.CompletedAtUtc.IsEmpty())
{
const int32 DurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds(
ActionPlan.CreatedAtUtc,
ActionPlan.CompletedAtUtc
);
if (DurationMs > 0)
{
CompletedPlanDurationAccumulator += DurationMs;
CompletedPlanDurationCount += 1;
if (Summary.BestCompletedPlanDurationMs == 0 || DurationMs < Summary.BestCompletedPlanDurationMs)
{
Summary.BestCompletedPlanDurationMs = DurationMs;
}
}
if (Summary.LastCompletedPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ActionPlan.CompletedAtUtc, LatestCompletedUtc) > 0)
{
Summary.LastCompletedPlanId = ActionPlan.PlanId;
LatestCompletedUtc = ActionPlan.CompletedAtUtc;
if (Summary.CurrentPlanId.IsEmpty())
{
Summary.FocusDeckId = ActionPlan.FocusDeckId;
Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId;
}
}
}
if (ActionPlan.bNeedsCoachReview
|| ActionPlan.bNeedsCadenceRecovery
|| ActionPlan.bNeedsVarietyRotation
|| ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted)
{
Summary.bNeedsCoachIntervention = true;
}
}
Summary.AveragePriorityScore = PriorityScoreAccumulator / static_cast<float>(Summary.TotalPlanCount);
if (CompletedPlanDurationCount > 0)
{
Summary.AverageCompletedPlanDurationMs =
CompletedPlanDurationAccumulator / CompletedPlanDurationCount;
}
int32 BestModeCount = 0;
for (int32 Index = 0; Index < UE_ARRAY_COUNT(SuggestedModeCounts); ++Index)
{
if (SuggestedModeCounts[Index] > BestModeCount)
{
BestModeCount = SuggestedModeCounts[Index];
Summary.DominantSuggestedMode = static_cast<EHyperTwistTrainingDeliveryMode>(Index);
}
}
int32 BestHandoffKindCount = 0;
for (int32 Index = 0; Index < UE_ARRAY_COUNT(HandoffKindCounts); ++Index)
{
if (HandoffKindCounts[Index] > BestHandoffKindCount)
{
BestHandoffKindCount = HandoffKindCounts[Index];
Summary.DominantPreferredCoachHandoffKind =
static_cast<EHyperTwistTrainingCoachHandoffKind>(Index);
}
}
int32 BestFocusLaneCount = 0;
for (const TPair<FString, int32>& Pair : FocusLaneCounts)
{
if (Pair.Value > BestFocusLaneCount)
{
BestFocusLaneCount = Pair.Value;
Summary.DominantFocusLaneLabel = Pair.Key;
}
}
int32 BestActionLabelCount = 0;
for (const TPair<FString, int32>& Pair : ActionLabelCounts)
{
if (Pair.Value > BestActionLabelCount)
{
BestActionLabelCount = Pair.Value;
Summary.DominantActionLabel = Pair.Key;
}
}
int32 BestHandoffSourceCount = 0;
for (const TPair<FString, int32>& Pair : HandoffSourceCounts)
{
if (Pair.Value > BestHandoffSourceCount)
{
BestHandoffSourceCount = Pair.Value;
Summary.DominantPreferredCoachHandoffSourceLabel = Pair.Key;
}
}
Summary.ActionPressureScore =
static_cast<float>(Summary.ActivePlanCount * 3)
+ static_cast<float>(Summary.DraftPlanCount * 2)
+ static_cast<float>(Summary.AbortedPlanCount * 2)
+ (static_cast<float>(Summary.OpenEntryCount) * 0.5f)
+ (Summary.bNeedsCoachIntervention ? 5.0f : 0.0f);
return Summary;
}
FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachActionPlan& ActionPlan,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionPlanOutcomeSummary Summary;
const FHyperTwistTrainingCoachActionPlan NormalizedPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan);
if (!NormalizedPlan.IsStructurallyValid())
{
return Summary;
}
Summary.UserId = NormalizedPlan.UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
!ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedPlan.ReferenceUtc
);
Summary.PlanId = NormalizedPlan.PlanId;
Summary.SourceSessionId = NormalizedPlan.SourceSessionId;
Summary.FocusDeckId = NormalizedPlan.FocusDeckId;
Summary.FocusMethodSegmentId = NormalizedPlan.FocusMethodSegmentId;
Summary.PrimaryActionLabel = NormalizedPlan.PrimaryActionLabel;
Summary.PlanState = NormalizedPlan.PlanState;
Summary.SuggestedMode = NormalizedPlan.SuggestedMode;
Summary.PreferredCoachHandoffKind = NormalizedPlan.PreferredCoachHandoffKind;
Summary.PreferredCoachHandoffSourceLabel = NormalizedPlan.PreferredCoachHandoffSourceLabel;
Summary.RequestedLaunchCaseCount = NormalizedPlan.RequestedLaunchCaseCount;
Summary.PlannedLaunchCaseCount = NormalizedPlan.PlannedLaunchCaseCount;
Summary.bBroadenedLaunchBudget = NormalizedPlan.bBroadenedLaunchBudget;
Summary.bTightenedLaunchBudget = NormalizedPlan.bTightenedLaunchBudget;
Summary.LaunchBudgetReasonLine = NormalizedPlan.LaunchBudgetReasonLine;
Summary.DeferredLaunchCaseIds = NormalizedPlan.DeferredLaunchCaseIds;
TSet<FString> FocusCaseIds;
for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty())
{
FocusCaseIds.Add(FocusCaseId);
}
}
TSet<FString> CompletedCaseIds;
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries)
{
if (!Entry.CaseId.IsEmpty() && FocusCaseIds.Num() == 0)
{
FocusCaseIds.Add(Entry.CaseId);
}
if (Entry.bCompleted && !Entry.CaseId.IsEmpty())
{
CompletedCaseIds.Add(Entry.CaseId);
}
}
Summary.FocusCaseCount = FocusCaseIds.Num();
FHyperTwistTrainingRunRecord RunRecord;
if (!NormalizedPlan.SourceSessionId.IsEmpty()
&& TryGetRunRecord(RepositoryState, NormalizedPlan.SourceSessionId, RunRecord))
{
TSet<FString> TouchedCaseIds;
int32 TotalTimeAccumulator = 0;
int32 TotalTimeSampleCount = 0;
int32 RecognitionTimeAccumulator = 0;
int32 RecognitionTimeSampleCount = 0;
for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory)
{
if (FocusCaseIds.Num() > 0 && !FocusCaseIds.Contains(AttemptHistoryEntry.CaseId))
{
continue;
}
Summary.AttemptCount += 1;
TouchedCaseIds.Add(AttemptHistoryEntry.CaseId);
if (!AttemptHistoryEntry.ReplayId.IsEmpty())
{
Summary.ReplayBackedAttemptCount += 1;
}
if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted
|| AttemptHistoryEntry.RecognitionTimeMs > 0)
{
Summary.RecognitionAttemptCount += 1;
}
if (AttemptHistoryEntry.TotalTimeMs > 0)
{
TotalTimeAccumulator += AttemptHistoryEntry.TotalTimeMs;
TotalTimeSampleCount += 1;
if (Summary.BestTotalTimeMs == 0 || AttemptHistoryEntry.TotalTimeMs < Summary.BestTotalTimeMs)
{
Summary.BestTotalTimeMs = AttemptHistoryEntry.TotalTimeMs;
}
}
if (AttemptHistoryEntry.RecognitionTimeMs > 0)
{
RecognitionTimeAccumulator += AttemptHistoryEntry.RecognitionTimeMs;
RecognitionTimeSampleCount += 1;
}
switch (AttemptHistoryEntry.Result)
{
case EHyperTwistTrainingAttemptResult::Success:
Summary.SuccessCount += 1;
if (!AttemptHistoryEntry.CaseId.IsEmpty())
{
CompletedCaseIds.Add(AttemptHistoryEntry.CaseId);
}
break;
case EHyperTwistTrainingAttemptResult::Failure:
Summary.FailureCount += 1;
break;
case EHyperTwistTrainingAttemptResult::Timeout:
Summary.TimeoutCount += 1;
break;
case EHyperTwistTrainingAttemptResult::DNF:
Summary.DNFCount += 1;
break;
case EHyperTwistTrainingAttemptResult::Aborted:
Summary.AbortedCount += 1;
break;
default:
break;
}
}
Summary.TouchedFocusCaseCount = TouchedCaseIds.Num();
if (TotalTimeSampleCount > 0)
{
Summary.AverageTotalTimeMs = TotalTimeAccumulator / TotalTimeSampleCount;
}
if (RecognitionTimeSampleCount > 0)
{
Summary.AverageRecognitionTimeMs = RecognitionTimeAccumulator / RecognitionTimeSampleCount;
}
}
Summary.CompletedFocusCaseCount = CompletedCaseIds.Num();
Summary.UntouchedFocusCaseCount = FMath::Max(0, Summary.FocusCaseCount - Summary.TouchedFocusCaseCount);
for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty() && !CompletedCaseIds.Contains(FocusCaseId))
{
Summary.FollowUpCaseIds.Add(FocusCaseId);
}
}
if (Summary.FollowUpCaseIds.Num() == 0)
{
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries)
{
if (!Entry.CaseId.IsEmpty()
&& !CompletedCaseIds.Contains(Entry.CaseId)
&& !Summary.FollowUpCaseIds.Contains(Entry.CaseId))
{
Summary.FollowUpCaseIds.Add(Entry.CaseId);
}
}
}
if (Summary.FocusCaseCount > 0)
{
Summary.CompletionRate = static_cast<float>(Summary.CompletedFocusCaseCount)
/ static_cast<float>(Summary.FocusCaseCount);
}
const FString PlanStartUtc = !NormalizedPlan.StartedAtUtc.IsEmpty()
? NormalizedPlan.StartedAtUtc
: NormalizedPlan.CreatedAtUtc;
const FString PlanEndUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty()
? NormalizedPlan.CompletedAtUtc
: Summary.ReferenceUtc;
if (!PlanStartUtc.IsEmpty() && !PlanEndUtc.IsEmpty())
{
Summary.PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds(
PlanStartUtc,
PlanEndUtc
);
}
const bool bPlanTerminal =
NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed
|| NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted;
Summary.bMetPrimaryGoal =
bPlanTerminal
&& NormalizedPlan.PlanState != EHyperTwistTrainingCoachActionPlanState::Aborted
&& (Summary.FocusCaseCount == 0
? Summary.SuccessCount > 0
: Summary.CompletedFocusCaseCount >= Summary.FocusCaseCount);
Summary.OutcomeScore =
(Summary.CompletionRate * 100.0f)
+ (static_cast<float>(Summary.SuccessCount) * 4.0f)
- (static_cast<float>(Summary.FailureCount) * 2.0f)
- (static_cast<float>(Summary.TimeoutCount) * 3.0f)
- (static_cast<float>(Summary.DNFCount) * 4.0f)
- (static_cast<float>(Summary.AbortedCount) * 5.0f)
- (static_cast<float>(Summary.UntouchedFocusCaseCount) * 4.0f)
+ (static_cast<float>(Summary.ReplayBackedAttemptCount) * 0.5f);
Summary.bNeedsFollowUpPlan =
Summary.FollowUpCaseIds.Num() > 0
|| Summary.DeferredLaunchCaseIds.Num() > 0
|| (
bPlanTerminal
&& (
NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted
|| !Summary.bMetPrimaryGoal
|| Summary.FailureCount > 0
|| Summary.TimeoutCount > 0
|| Summary.DNFCount > 0
|| (NormalizedPlan.bNeedsCoachReview && Summary.ReplayBackedAttemptCount == 0)
)
);
return Summary;
}
FHyperTwistTrainingCoachActionSequenceSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionSequenceSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachActionPlan& AnchorPlan,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachActionSequenceSummary Summary;
const FHyperTwistTrainingCoachActionPlan NormalizedAnchorPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(AnchorPlan);
if (!NormalizedAnchorPlan.IsStructurallyValid())
{
return Summary;
}
const FString RootPlanId = !NormalizedAnchorPlan.RootPlanId.IsEmpty()
? NormalizedAnchorPlan.RootPlanId
: NormalizedAnchorPlan.PlanId;
Summary.UserId = NormalizedAnchorPlan.UserId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
!ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedAnchorPlan.ReferenceUtc
);
Summary.RootPlanId = RootPlanId;
const TArray<FHyperTwistTrainingCoachActionPlan> UserPlans = ListCoachActionPlansForUser(
RepositoryState,
NormalizedAnchorPlan.UserId
);
TArray<FHyperTwistTrainingCoachActionPlan> SequencePlans;
for (const FHyperTwistTrainingCoachActionPlan& Plan : UserPlans)
{
const FHyperTwistTrainingCoachActionPlan NormalizedPlan =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(Plan);
const FString CandidateRootPlanId = !NormalizedPlan.RootPlanId.IsEmpty()
? NormalizedPlan.RootPlanId
: NormalizedPlan.PlanId;
if (CandidateRootPlanId == RootPlanId
|| NormalizedPlan.PlanId == RootPlanId
|| NormalizedPlan.ParentPlanId == RootPlanId)
{
SequencePlans.Add(NormalizedPlan);
}
}
if (SequencePlans.Num() == 0)
{
SequencePlans.Add(NormalizedAnchorPlan);
}
const auto ResolvePlanSortUtc = [](const FHyperTwistTrainingCoachActionPlan& Plan) -> FString
{
return !Plan.CompletedAtUtc.IsEmpty()
? Plan.CompletedAtUtc
: (!Plan.StartedAtUtc.IsEmpty() ? Plan.StartedAtUtc : Plan.CreatedAtUtc);
};
SequencePlans.Sort(
[&ResolvePlanSortUtc](const FHyperTwistTrainingCoachActionPlan& Left,
const FHyperTwistTrainingCoachActionPlan& Right)
{
const FString LeftSortUtc = ResolvePlanSortUtc(Left);
const FString RightSortUtc = ResolvePlanSortUtc(Right);
const int32 CompareResult =
HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(LeftSortUtc, RightSortUtc);
if (CompareResult != 0)
{
return CompareResult < 0;
}
if (Left.FollowUpDepth != Right.FollowUpDepth)
{
return Left.FollowUpDepth < Right.FollowUpDepth;
}
return Left.PlanId < Right.PlanId;
}
);
FString LatestSortUtc;
TSet<FString> OutstandingFollowUpCases;
float CompletionRateAccumulator = 0.0f;
float OutcomeScoreAccumulator = 0.0f;
bool bHasGenericFollowUpPressure = false;
for (const FHyperTwistTrainingCoachActionPlan& Plan : SequencePlans)
{
Summary.PlanCount += 1;
Summary.CarryForwardPlanCount += Plan.FollowUpDepth > 0 ? 1 : 0;
Summary.MaxFollowUpDepth = FMath::Max(Summary.MaxFollowUpDepth, Plan.FollowUpDepth);
switch (Plan.PlanState)
{
case EHyperTwistTrainingCoachActionPlanState::Active:
Summary.ActivePlanCount += 1;
Summary.bHasOpenPlan = true;
break;
case EHyperTwistTrainingCoachActionPlanState::Completed:
Summary.CompletedPlanCount += 1;
break;
case EHyperTwistTrainingCoachActionPlanState::Aborted:
Summary.AbortedPlanCount += 1;
break;
default:
Summary.bHasOpenPlan = true;
break;
}
const FHyperTwistTrainingCoachActionPlanOutcomeSummary OutcomeSummary =
DeriveCoachActionPlanOutcomeSummary(RepositoryState, Plan, Summary.ReferenceUtc);
Summary.TotalFocusCaseCount +=
OutcomeSummary.FocusCaseCount + OutcomeSummary.DeferredLaunchCaseIds.Num();
CompletionRateAccumulator += OutcomeSummary.CompletionRate;
OutcomeScoreAccumulator += OutcomeSummary.OutcomeScore;
TSet<FString> PlanFocusCaseIds;
for (const FString& FocusCaseId : Plan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty())
{
PlanFocusCaseIds.Add(FocusCaseId);
}
}
for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : Plan.Entries)
{
if (!Entry.CaseId.IsEmpty())
{
PlanFocusCaseIds.Add(Entry.CaseId);
}
}
TSet<FString> PlanOutstandingCaseIds;
for (const FString& CaseId : OutcomeSummary.FollowUpCaseIds)
{
if (!CaseId.IsEmpty())
{
PlanOutstandingCaseIds.Add(CaseId);
OutstandingFollowUpCases.Add(CaseId);
}
}
for (const FString& CaseId : OutcomeSummary.DeferredLaunchCaseIds)
{
if (!CaseId.IsEmpty())
{
PlanOutstandingCaseIds.Add(CaseId);
OutstandingFollowUpCases.Add(CaseId);
}
}
for (const FString& FocusCaseId : PlanFocusCaseIds)
{
if (!PlanOutstandingCaseIds.Contains(FocusCaseId))
{
OutstandingFollowUpCases.Remove(FocusCaseId);
}
}
bHasGenericFollowUpPressure =
OutcomeSummary.bNeedsFollowUpPlan && PlanOutstandingCaseIds.Num() == 0;
const FString SortUtc = ResolvePlanSortUtc(Plan);
if (Summary.LatestPlanId.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) > 0
|| (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestSortUtc) == 0
&& Plan.FollowUpDepth >= Summary.MaxFollowUpDepth))
{
Summary.LatestPlanId = Plan.PlanId;
Summary.FocusDeckId = Plan.FocusDeckId;
Summary.FocusMethodSegmentId = Plan.FocusMethodSegmentId;
Summary.LatestOutcomeScore = OutcomeSummary.OutcomeScore;
LatestSortUtc = SortUtc;
}
}
if (Summary.PlanCount > 0)
{
Summary.AverageCompletionRate = CompletionRateAccumulator / static_cast<float>(Summary.PlanCount);
Summary.AverageOutcomeScore = OutcomeScoreAccumulator / static_cast<float>(Summary.PlanCount);
}
Summary.UnresolvedFollowUpCaseCount = OutstandingFollowUpCases.Num();
Summary.bNeedsMoreFollowUp =
Summary.UnresolvedFollowUpCaseCount > 0 || bHasGenericFollowUpPressure || Summary.bHasOpenPlan;
return Summary;
}

View file

@ -10,6 +10,8 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
FString ResolveReferenceUtc(const FString& ReferenceUtc);
int32 ComputeElapsedMilliseconds(const FString& OlderUtc, const FString& NewerUtc);
int32 ComputePlanDurationMs(
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
const FString& ReferenceUtc

View file

@ -1679,3 +1679,45 @@ Latest native repository coach-workflow split follow-up later on `2026-06-29`:
same-family hotspot
- the next worthwhile continuation remains another bounded training-repository
seam rather than reopening product topology or renderer widening
Latest native repository coach-summary split follow-up later on `2026-06-29`:
- the next same-family continuation kept shrinking the remaining
training-repository hotspot without widening simulator or website scope:
- moved the coach action-plan summary, coach action-plan outcome summary,
and coach action-sequence summary family out of
`HyperTwistTrainingRepositoryLibrary.cpp`
- added the extracted implementation unit at
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp`
- widened the existing private internal declaration seam by one more bounded
helper declaration so the extracted summary family can reuse
`ComputeElapsedMilliseconds(...)` without reopening broader helper
exposure
- reduced the main repository file to `30,471` lines while the extracted
coach-summary unit now holds `625` lines
- the HyperTwist-owned structural loop stayed green on that exact native
packet:
- `scripts/run-hypertwist-sentrux-source-only.sh`
- `Quality: 6233`
- all `7` rules pass
- `scripts/run-hypertwist-sentrux-gate.sh`
- comparison result:
- `Quality: 6234 -> 6233`
- `Coupling: 0.15 -> 0.15`
- `Cycles: 0 -> 0`
- `God files: 1 -> 1`
- `No degradation detected`
- the canonical Windows Unreal proof for that native packet then passed
through the maintained reverse-SSH lane:
- reran `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'`
- `Target is up to date`
- `Result: Succeeded`
- UnrealBuildTool `Total execution time: 5.67 seconds`
- current truthful reading after this native follow-up:
- the coach-summary extraction is now build-proven on the canonical Windows
lane and safe to land
- vanilla refactoring still is not fully complete because the training
repository library still remains one of the real native hotspot files
- the next worthwhile continuation remains another bounded
training-repository or validation-library seam, not fresh product-family
widening

View file

@ -2501,3 +2501,46 @@ Latest same-family responsive public-route expansion follow-up on `2026-06-29`:
repository library still contains the remaining same-family hotspot
- the next worthwhile continuation remains another bounded training-repository
seam instead of fresh product-topology widening
## Latest native repository coach-summary split follow-up later on `2026-06-29`
- the next same-family continuation then narrowed the remaining
training-repository hotspot again without widening simulator or website
scope:
- the coach action-plan summary, coach action-plan outcome summary, and
coach action-sequence summary family was moved out of
`HyperTwistTrainingRepositoryLibrary.cpp`
- the extracted implementation family now lives at
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryCoachSummaries.cpp`
- the existing private helper declaration seam was widened by one more
bounded helper declaration so the extracted summary family can reuse
`ComputeElapsedMilliseconds(...)` without reopening broader helper
exposure
- the main repository file now stands at `30,471` lines and the extracted
coach-summary unit stands at `625` lines
- the HyperTwist-owned structural loop stayed healthy on that exact native
state:
- `scripts/run-hypertwist-sentrux-source-only.sh`
- `Quality: 6233`
- all `7` rules pass
- `scripts/run-hypertwist-sentrux-gate.sh`
- comparison result:
- `Quality: 6234 -> 6233`
- `Coupling: 0.15 -> 0.15`
- `Cycles: 0 -> 0`
- `God files: 1 -> 1`
- `No degradation detected`
- the canonical Windows Unreal proof for that native split then passed through
the maintained reverse-SSH lane:
- `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'`
- `Target is up to date`
- `Result: Succeeded`
- UnrealBuildTool `Total execution time: 5.67 seconds`
- current truthful reading after this native follow-up:
- the coach-summary extraction is now build-proven on the canonical Windows
lane
- vanilla refactoring still is not fully complete because the training
repository library remains one of the real native hotspot files
- the next worthwhile continuation remains another bounded
training-repository or validation-library seam instead of fresh
product-topology widening