Split HyperTwist coach action summary repository family
This commit is contained in:
parent
c6a88aa9eb
commit
677fbb5dbb
5 changed files with 712 additions and 618 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue