Add coach queue execution analytics

This commit is contained in:
axiomlogicnexus 2026-04-26 04:45:03 +02:00
parent 1a309c862c
commit f311b65f84
9 changed files with 845 additions and 4 deletions

View file

@ -1367,6 +1367,72 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistContractLibrary::MakeSampleTrai
);
}
FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueExecutionSummary()
{
FHyperTwistTrainingRepositoryState RepositoryState = MakeSampleTrainingRepositoryState();
FHyperTwistTrainingCoachSessionQueueState QueueState =
UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
MakeSampleTrainingCoachSessionQueueSummary(),
TEXT("coach_queue_active"),
TEXT("2026-04-28T12:12:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
QueueState
);
QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry(
QueueState,
TEXT("coach_queue_follow_up"),
TEXT("2026-04-28T18:00:00Z"),
TEXT("follow-up-held-behind-primary"),
false,
TEXT("2026-04-28T12:13:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
QueueState
);
QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry(
QueueState,
TEXT("coach_queue_archive"),
TEXT("2026-04-29T09:00:00Z"),
TEXT("archive-retention-window"),
true,
TEXT("2026-04-28T12:14:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
QueueState
);
QueueState = UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel(
QueueState,
TEXT("primary-brief"),
TEXT("coach_training_session_queued_01"),
TEXT("2026-04-28T12:20:00Z")
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
QueueState
);
QueueState = UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry(
QueueState,
TEXT("coach_training_session_queued_01"),
TEXT("2026-04-28T12:34:00Z"),
false
);
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
QueueState
);
return UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary(
RepositoryState,
TEXT("local-user"),
TEXT("coach_queue_active"),
TEXT("2026-04-28T12:40:00Z")
);
}
bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State)
{
return State.IsStructurallyValid();

View file

@ -704,6 +704,147 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return NormalizedState;
}
bool DidCoachSessionQueueEntryChange(
const FHyperTwistTrainingCoachSessionQueueStateEntry& PreviousEntry,
const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry
)
{
return PreviousEntry.WorkFingerprint != CurrentEntry.WorkFingerprint
|| PreviousEntry.EntryState != CurrentEntry.EntryState
|| PreviousEntry.ScheduledAtUtc != CurrentEntry.ScheduledAtUtc
|| PreviousEntry.DeferredUntilUtc != CurrentEntry.DeferredUntilUtc
|| PreviousEntry.DeferredAtUtc != CurrentEntry.DeferredAtUtc
|| PreviousEntry.DeferralReasonLabel != CurrentEntry.DeferralReasonLabel
|| PreviousEntry.StartedAtUtc != CurrentEntry.StartedAtUtc
|| PreviousEntry.CompletedAtUtc != CurrentEntry.CompletedAtUtc
|| PreviousEntry.RescheduleCount != CurrentEntry.RescheduleCount
|| PreviousEntry.SourceTrainingSessionId != CurrentEntry.SourceTrainingSessionId
|| PreviousEntry.bWasManuallyRescheduled != CurrentEntry.bWasManuallyRescheduled;
}
EHyperTwistTrainingCoachSessionQueueEventKind DeriveCoachSessionQueueEventKind(
const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry,
const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry
)
{
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
return EHyperTwistTrainingCoachSessionQueueEventKind::Started;
}
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred)
{
return EHyperTwistTrainingCoachSessionQueueEventKind::Deferred;
}
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Completed)
{
return EHyperTwistTrainingCoachSessionQueueEventKind::Completed;
}
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Skipped)
{
return EHyperTwistTrainingCoachSessionQueueEventKind::Skipped;
}
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired)
{
return EHyperTwistTrainingCoachSessionQueueEventKind::Expired;
}
if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready)
{
return PreviousEntry != nullptr
&& PreviousEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
? EHyperTwistTrainingCoachSessionQueueEventKind::Promoted
: EHyperTwistTrainingCoachSessionQueueEventKind::Ready;
}
return EHyperTwistTrainingCoachSessionQueueEventKind::Queued;
}
FString ResolveCoachSessionQueueHistoryEventAtUtc(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry,
const EHyperTwistTrainingCoachSessionQueueEventKind EventKind
)
{
switch (EventKind)
{
case EHyperTwistTrainingCoachSessionQueueEventKind::Started:
return ResolveReferenceUtc(!Entry.StartedAtUtc.IsEmpty() ? Entry.StartedAtUtc : QueueState.LastUpdatedAtUtc);
case EHyperTwistTrainingCoachSessionQueueEventKind::Deferred:
return ResolveReferenceUtc(!Entry.DeferredAtUtc.IsEmpty() ? Entry.DeferredAtUtc : QueueState.LastUpdatedAtUtc);
case EHyperTwistTrainingCoachSessionQueueEventKind::Completed:
case EHyperTwistTrainingCoachSessionQueueEventKind::Skipped:
case EHyperTwistTrainingCoachSessionQueueEventKind::Expired:
return ResolveReferenceUtc(!Entry.CompletedAtUtc.IsEmpty() ? Entry.CompletedAtUtc : QueueState.LastUpdatedAtUtc);
case EHyperTwistTrainingCoachSessionQueueEventKind::Promoted:
case EHyperTwistTrainingCoachSessionQueueEventKind::Ready:
return ResolveReferenceUtc(!Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : QueueState.LastUpdatedAtUtc);
default:
return ResolveReferenceUtc(QueueState.LastUpdatedAtUtc);
}
}
FString MakeCoachSessionQueueHistoryEntryId(
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Entry
)
{
return FString::Printf(
TEXT("%s|%s|%d|%s|%s|%s"),
*Entry.QueueId,
*Entry.EntryId,
static_cast<int32>(Entry.EventKind),
*Entry.EventAtUtc,
*Entry.WorkFingerprint,
*Entry.SourceTrainingSessionId
);
}
FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizeCoachSessionQueueHistoryEntry(
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry
)
{
FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedEntry = HistoryEntry;
NormalizedEntry.EventAtUtc = ResolveReferenceUtc(NormalizedEntry.EventAtUtc);
NormalizedEntry.FocusCaseIds = DeduplicateCaseIds(NormalizedEntry.FocusCaseIds);
if (NormalizedEntry.HistoryEntryId.IsEmpty())
{
NormalizedEntry.HistoryEntryId = MakeCoachSessionQueueHistoryEntryId(NormalizedEntry);
}
return NormalizedEntry;
}
FHyperTwistTrainingCoachSessionQueueHistoryEntry BuildCoachSessionQueueHistoryEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry,
const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry
)
{
FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry;
HistoryEntry.QueueId = QueueState.QueueId;
HistoryEntry.UserId = QueueState.UserId;
HistoryEntry.EntryId = CurrentEntry.EntryId;
HistoryEntry.SourceLabel = CurrentEntry.SourceLabel;
HistoryEntry.WorkFingerprint = CurrentEntry.WorkFingerprint;
HistoryEntry.EventKind = DeriveCoachSessionQueueEventKind(PreviousEntry, CurrentEntry);
HistoryEntry.EventAtUtc = ResolveCoachSessionQueueHistoryEventAtUtc(
QueueState,
CurrentEntry,
HistoryEntry.EventKind
);
HistoryEntry.PreviousEntryState = PreviousEntry != nullptr
? PreviousEntry->EntryState
: CurrentEntry.EntryState;
HistoryEntry.NewEntryState = CurrentEntry.EntryState;
HistoryEntry.SourceTrainingSessionId = CurrentEntry.SourceTrainingSessionId;
HistoryEntry.DeferredUntilUtc = CurrentEntry.DeferredUntilUtc;
HistoryEntry.DeferralReasonLabel = CurrentEntry.DeferralReasonLabel;
HistoryEntry.FocusDeckId = CurrentEntry.FocusDeckId;
HistoryEntry.FocusMethodSegmentId = CurrentEntry.FocusMethodSegmentId;
HistoryEntry.FocusCaseIds = CurrentEntry.FocusCaseIds;
HistoryEntry.bRequiresFollowUp = CurrentEntry.bRequiresFollowUp;
HistoryEntry.bRequiresArchiveRetentionTuning = CurrentEntry.bRequiresArchiveRetentionTuning;
HistoryEntry.bWasManualReschedule = CurrentEntry.bWasManuallyRescheduled;
return NormalizeCoachSessionQueueHistoryEntry(HistoryEntry);
}
FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory(
const FString& UserId,
const FString& DeckId,
@ -2932,6 +3073,26 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
SeenCoachSessionQueueKeys.Add(QueueKey);
}
Report.TotalCoachSessionQueueHistoryCount = RepositoryState.StoredCoachSessionQueueHistory.Num();
TSet<FString> SeenCoachSessionQueueHistoryIds;
for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory)
{
const FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedHistoryEntry =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueHistoryEntry(HistoryEntry);
if (!NormalizedHistoryEntry.IsStructurallyValid())
{
Report.InvalidCoachSessionQueueHistoryCount += 1;
continue;
}
if (SeenCoachSessionQueueHistoryIds.Contains(NormalizedHistoryEntry.HistoryEntryId))
{
Report.DuplicateCoachSessionQueueHistoryCount += 1;
continue;
}
SeenCoachSessionQueueHistoryIds.Add(NormalizedHistoryEntry.HistoryEntryId);
}
if (Report.InvalidRunRecordCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
@ -3047,6 +3208,20 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
TEXT("normalize-and-deduplicate-coach-session-queues")
);
}
if (Report.InvalidCoachSessionQueueHistoryCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-coach-session-queue-history")
);
}
if (Report.DuplicateCoachSessionQueueHistoryCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("normalize-and-deduplicate-coach-session-queue-history")
);
}
return Report;
}
@ -3409,6 +3584,55 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR
return Left.QueueId < Right.QueueId;
});
TMap<FString, int32> CoachSessionQueueHistoryIndexById;
for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory)
{
const FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedHistoryEntry =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueHistoryEntry(HistoryEntry);
if (!NormalizedHistoryEntry.IsStructurallyValid())
{
continue;
}
if (const int32* ExistingIndex = CoachSessionQueueHistoryIndexById.Find(NormalizedHistoryEntry.HistoryEntryId))
{
RepairedState.StoredCoachSessionQueueHistory[*ExistingIndex] = NormalizedHistoryEntry;
}
else
{
CoachSessionQueueHistoryIndexById.Add(
NormalizedHistoryEntry.HistoryEntryId,
RepairedState.StoredCoachSessionQueueHistory.Add(NormalizedHistoryEntry)
);
}
}
RepairedState.StoredCoachSessionQueueHistory.Sort([](
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left,
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right
)
{
if (Left.UserId != Right.UserId)
{
return Left.UserId < Right.UserId;
}
if (Left.QueueId != Right.QueueId)
{
return Left.QueueId < Right.QueueId;
}
const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
Left.EventAtUtc,
Right.EventAtUtc
);
if (UtcCompare != 0)
{
return UtcCompare < 0;
}
return Left.HistoryEntryId < Right.HistoryEntryId;
});
return RepairedState;
}
@ -5657,6 +5881,47 @@ TArray<FHyperTwistTrainingCoachSessionQueueState> UHyperTwistTrainingRepositoryL
return QueueStates;
}
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> HistoryEntries;
if (UserId.IsEmpty() || QueueId.IsEmpty())
{
return HistoryEntries;
}
for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory)
{
if (HistoryEntry.UserId == UserId
&& HistoryEntry.QueueId == QueueId
&& HistoryEntry.IsStructurallyValid())
{
HistoryEntries.Add(HistoryEntry);
}
}
HistoryEntries.Sort([](
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left,
const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right
)
{
const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
Left.EventAtUtc,
Right.EventAtUtc
);
if (UtcCompare != 0)
{
return UtcCompare < 0;
}
return Left.HistoryEntryId < Right.HistoryEntryId;
});
return HistoryEntries;
}
bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
@ -5701,7 +5966,7 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC
return UpdatedRepositoryState;
}
FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState =
const FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState =
UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate(
[&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate)
{
@ -5709,9 +5974,65 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC
&& Candidate.QueueId == NormalizedQueueState.QueueId;
}
);
if (ExistingQueueState != nullptr)
for (const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry : NormalizedQueueState.Entries)
{
*ExistingQueueState = NormalizedQueueState;
const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry = ExistingQueueState != nullptr
? ExistingQueueState->Entries.FindByPredicate(
[&CurrentEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == CurrentEntry.EntryId;
}
)
: nullptr;
if (PreviousEntry != nullptr && PreviousEntry->WorkFingerprint != CurrentEntry.WorkFingerprint)
{
PreviousEntry = nullptr;
}
if (PreviousEntry != nullptr
&& !HyperTwistTrainingRepositoryLibraryInternal::DidCoachSessionQueueEntryChange(
*PreviousEntry,
CurrentEntry
))
{
continue;
}
const FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry =
HyperTwistTrainingRepositoryLibraryInternal::BuildCoachSessionQueueHistoryEntry(
NormalizedQueueState,
CurrentEntry,
PreviousEntry
);
if (!HistoryEntry.IsStructurallyValid())
{
continue;
}
const bool bAlreadyExists =
UpdatedRepositoryState.StoredCoachSessionQueueHistory.ContainsByPredicate(
[&HistoryEntry](const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Candidate)
{
return Candidate.HistoryEntryId == HistoryEntry.HistoryEntryId;
}
);
if (!bAlreadyExists)
{
UpdatedRepositoryState.StoredCoachSessionQueueHistory.Add(HistoryEntry);
}
}
FHyperTwistTrainingCoachSessionQueueState* MutableExistingQueueState =
UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate(
[&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate)
{
return Candidate.UserId == NormalizedQueueState.UserId
&& Candidate.QueueId == NormalizedQueueState.QueueId;
}
);
if (MutableExistingQueueState != nullptr)
{
*MutableExistingQueueState = NormalizedQueueState;
}
else
{
@ -5721,6 +6042,202 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC
return UpdatedRepositoryState;
}
FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachSessionQueueExecutionSummary Summary;
if (UserId.IsEmpty() || QueueId.IsEmpty())
{
return Summary;
}
Summary.UserId = UserId;
Summary.QueueId = QueueId;
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
FHyperTwistTrainingCoachSessionQueueState QueueState;
if (TryGetCoachSessionQueueState(RepositoryState, UserId, QueueId, QueueState))
{
Summary.FocusDeckId = QueueState.FocusDeckId;
Summary.FocusMethodSegmentId = QueueState.FocusMethodSegmentId;
Summary.bHasOutstandingDeferredWork =
QueueState.DeferredEntryCount > 0 || QueueState.PendingEntryCount > 0;
}
const TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> HistoryEntries =
ListCoachSessionQueueHistoryEntries(RepositoryState, UserId, QueueId);
if (HistoryEntries.Num() == 0)
{
return Summary;
}
Summary.TotalHistoryEntryCount = HistoryEntries.Num();
TSet<FString> UniqueEntryIds;
TMap<FString, int32> DeferredCountsByEntryId;
TMap<FString, int32> CompletedCountsByEntryId;
TMap<FString, int32> DeferralReasonCounts;
for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : HistoryEntries)
{
if (!HistoryEntry.IsStructurallyValid())
{
continue;
}
UniqueEntryIds.Add(HistoryEntry.EntryId);
if (Summary.FocusDeckId.IsEmpty())
{
Summary.FocusDeckId = HistoryEntry.FocusDeckId;
}
if (Summary.FocusMethodSegmentId.IsEmpty())
{
Summary.FocusMethodSegmentId = HistoryEntry.FocusMethodSegmentId;
}
if (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
HistoryEntry.EventAtUtc,
Summary.ReferenceUtc
) > 0)
{
continue;
}
switch (HistoryEntry.EventKind)
{
case EHyperTwistTrainingCoachSessionQueueEventKind::Queued:
Summary.QueuedEventCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Ready:
Summary.ReadyEventCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Started:
Summary.StartedEventCount += 1;
if (Summary.LastStartedAtUtc.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
HistoryEntry.EventAtUtc,
Summary.LastStartedAtUtc
) > 0)
{
Summary.LastStartedAtUtc = HistoryEntry.EventAtUtc;
}
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Deferred:
Summary.DeferredEventCount += 1;
DeferredCountsByEntryId.FindOrAdd(HistoryEntry.EntryId) += 1;
if (!HistoryEntry.DeferralReasonLabel.IsEmpty())
{
DeferralReasonCounts.FindOrAdd(HistoryEntry.DeferralReasonLabel) += 1;
}
if (Summary.LastDeferredAtUtc.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
HistoryEntry.EventAtUtc,
Summary.LastDeferredAtUtc
) > 0)
{
Summary.LastDeferredAtUtc = HistoryEntry.EventAtUtc;
}
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Promoted:
Summary.PromotedEventCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Completed:
Summary.CompletedEventCount += 1;
CompletedCountsByEntryId.FindOrAdd(HistoryEntry.EntryId) += 1;
if (Summary.LastCompletedAtUtc.IsEmpty()
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
HistoryEntry.EventAtUtc,
Summary.LastCompletedAtUtc
) > 0)
{
Summary.LastCompletedAtUtc = HistoryEntry.EventAtUtc;
}
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Skipped:
Summary.SkippedEventCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEventKind::Expired:
Summary.ExpiredEventCount += 1;
break;
default:
break;
}
if (HistoryEntry.bWasManualReschedule)
{
Summary.ManualRescheduleEventCount += 1;
}
if (HistoryEntry.bRequiresArchiveRetentionTuning)
{
Summary.ArchiveAwareEventCount += 1;
}
if (HistoryEntry.bRequiresFollowUp
&& HistoryEntry.EventKind == EHyperTwistTrainingCoachSessionQueueEventKind::Deferred)
{
Summary.BlockedFollowUpEventCount += 1;
}
}
Summary.UniqueEntryCount = UniqueEntryIds.Num();
int32 DominantDeferredCount = 0;
for (const TPair<FString, int32>& Pair : DeferredCountsByEntryId)
{
if (Pair.Value > DominantDeferredCount
|| (Pair.Value == DominantDeferredCount
&& (Summary.MostDeferredEntryId.IsEmpty() || Pair.Key < Summary.MostDeferredEntryId)))
{
DominantDeferredCount = Pair.Value;
Summary.MostDeferredEntryId = Pair.Key;
}
}
int32 DominantCompletedCount = 0;
for (const TPair<FString, int32>& Pair : CompletedCountsByEntryId)
{
if (Pair.Value > DominantCompletedCount
|| (Pair.Value == DominantCompletedCount
&& (Summary.MostCompletedEntryId.IsEmpty() || Pair.Key < Summary.MostCompletedEntryId)))
{
DominantCompletedCount = Pair.Value;
Summary.MostCompletedEntryId = Pair.Key;
}
}
int32 DominantReasonCount = 0;
for (const TPair<FString, int32>& Pair : DeferralReasonCounts)
{
if (Pair.Value > DominantReasonCount
|| (Pair.Value == DominantReasonCount
&& (Summary.DominantDeferralReasonLabel.IsEmpty() || Pair.Key < Summary.DominantDeferralReasonLabel)))
{
DominantReasonCount = Pair.Value;
Summary.DominantDeferralReasonLabel = Pair.Key;
}
}
const int32 TerminalEventCount =
Summary.CompletedEventCount + Summary.SkippedEventCount + Summary.ExpiredEventCount;
Summary.bHasCompletionGap = Summary.StartedEventCount > TerminalEventCount;
Summary.bHasArchiveAwareFriction = Summary.ArchiveAwareEventCount > 0;
Summary.bHasBlockedFollowUpFriction = Summary.BlockedFollowUpEventCount > 0;
Summary.bHasRepeatManualRescheduleFriction = Summary.ManualRescheduleEventCount >= 2;
Summary.ExecutionPressureScore = FMath::Clamp(
static_cast<float>(Summary.DeferredEventCount) * 7.0f
+ static_cast<float>(Summary.ManualRescheduleEventCount) * 10.0f
+ static_cast<float>(Summary.ArchiveAwareEventCount) * 12.0f
+ static_cast<float>(Summary.BlockedFollowUpEventCount) * 12.0f
+ (Summary.bHasCompletionGap ? 10.0f : 0.0f),
0.0f,
100.0f
);
return Summary;
}
FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,

View file

@ -405,6 +405,18 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingRuntimeLibrary::GetActi
return FHyperTwistCoachSchedulePolicySummary();
}
FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueExecutionSummary(
UObject* WorldContextObject
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveCoachSessionQueueExecutionSummary();
}
return FHyperTwistTrainingCoachSessionQueueExecutionSummary();
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))

View file

@ -183,6 +183,11 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingSubsystem::GetActiveCoa
return ActiveCoachSchedulePolicySummary;
}
FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueExecutionSummary() const
{
return ActiveCoachSessionQueueExecutionSummary;
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const
{
return ActiveLearnerMethodSegments;
@ -1385,6 +1390,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary();
ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary();
ActiveCoachSessionQueueExecutionSummary = FHyperTwistTrainingCoachSessionQueueExecutionSummary();
ActiveLearnerMethodSegments.Reset();
ActiveLearnerPresets.Reset();
ActiveTrainingSessionTemplates.Reset();
@ -1598,12 +1604,20 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachArchivePolicySummary,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
ActiveCoachSessionQueueExecutionSummary =
UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary(
TrainingRepositoryState,
ActiveCoachSessionQueueState.UserId,
ActiveCoachSessionQueueState.QueueId,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
}
else
{
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary();
ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary();
ActiveCoachSessionQueueExecutionSummary = FHyperTwistTrainingCoachSessionQueueExecutionSummary();
}
}

View file

@ -218,6 +218,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSchedulePolicySummary MakeSampleTrainingCoachSchedulePolicySummary();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistTrainingCoachSessionQueueExecutionSummary MakeSampleTrainingCoachSessionQueueExecutionSummary();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation")
static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State);

View file

@ -400,6 +400,13 @@ public:
const FString& UserId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> ListCoachSessionQueueHistoryEntries(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Coach")
static bool TryGetCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
@ -414,6 +421,14 @@ public:
const FHyperTwistTrainingCoachSessionQueueState& QueueState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueExecutionSummary DeriveCoachSessionQueueExecutionSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachActionPlanSummary DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,

View file

@ -135,6 +135,11 @@ public:
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingCoachSessionQueueExecutionSummary GetActiveTrainingCoachSessionQueueExecutionSummary(
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static TArray<FHyperTwistTrainingMethodSegment> GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject);

View file

@ -116,6 +116,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistCoachSchedulePolicySummary GetActiveCoachSchedulePolicySummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingCoachSessionQueueExecutionSummary GetActiveCoachSessionQueueExecutionSummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
TArray<FHyperTwistTrainingMethodSegment> GetActiveLearnerMethodSegments() const;
@ -498,6 +501,9 @@ private:
UPROPERTY()
FHyperTwistCoachSchedulePolicySummary ActiveCoachSchedulePolicySummary;
UPROPERTY()
FHyperTwistTrainingCoachSessionQueueExecutionSummary ActiveCoachSessionQueueExecutionSummary;
UPROPERTY()
TArray<FHyperTwistTrainingMethodSegment> ActiveLearnerMethodSegments;

View file

@ -2730,6 +2730,19 @@ enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8
Expired UMETA(DisplayName = "Expired")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingCoachSessionQueueEventKind : uint8
{
Queued UMETA(DisplayName = "Queued"),
Ready UMETA(DisplayName = "Ready"),
Started UMETA(DisplayName = "Started"),
Deferred UMETA(DisplayName = "Deferred"),
Promoted UMETA(DisplayName = "Promoted"),
Completed UMETA(DisplayName = "Completed"),
Skipped UMETA(DisplayName = "Skipped"),
Expired UMETA(DisplayName = "Expired")
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachActionPlanEntry
{
@ -3035,6 +3048,182 @@ struct FHyperTwistTrainingCoachSessionQueueState
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachSessionQueueHistoryEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString HistoryEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString QueueId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SourceLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString WorkFingerprint;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingCoachSessionQueueEventKind EventKind =
EHyperTwistTrainingCoachSessionQueueEventKind::Queued;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingCoachSessionQueueEntryState PreviousEntryState =
EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingCoachSessionQueueEntryState NewEntryState =
EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SourceTrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeferredUntilUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeferralReasonLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusDeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusMethodSegmentId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> FocusCaseIds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresFollowUp = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresArchiveRetentionTuning = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bWasManualReschedule = false;
bool IsStructurallyValid() const
{
return !HistoryEntryId.IsEmpty()
&& !QueueId.IsEmpty()
&& !UserId.IsEmpty()
&& !EntryId.IsEmpty()
&& !EventAtUtc.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachSessionQueueExecutionSummary
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString QueueId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReferenceUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusDeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusMethodSegmentId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalHistoryEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 UniqueEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 QueuedEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ReadyEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 StartedEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DeferredEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 PromotedEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 CompletedEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SkippedEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpiredEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ManualRescheduleEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ArchiveAwareEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 BlockedFollowUpEventCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MostDeferredEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString MostCompletedEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DominantDeferralReasonLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastStartedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastDeferredAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastCompletedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float ExecutionPressureScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasOutstandingDeferredWork = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasCompletionGap = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasArchiveAwareFriction = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasBlockedFollowUpFriction = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasRepeatManualRescheduleFriction = false;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && !QueueId.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachActionPlanSummary
{
@ -3703,6 +3892,9 @@ struct FHyperTwistTrainingRepositoryState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachSessionQueueState> StoredCoachSessionQueues;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachSessionQueueHistoryEntry> StoredCoachSessionQueueHistory;
};
USTRUCT(BlueprintType)
@ -3782,6 +3974,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateCoachSessionQueueCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalCoachSessionQueueHistoryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidCoachSessionQueueHistoryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateCoachSessionQueueHistoryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> RepairActions;
@ -3802,6 +4003,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|| InvalidCoachActionPlanCount > 0
|| DuplicateCoachActionPlanCount > 0
|| InvalidCoachSessionQueueCount > 0
|| DuplicateCoachSessionQueueCount > 0;
|| DuplicateCoachSessionQueueCount > 0
|| InvalidCoachSessionQueueHistoryCount > 0
|| DuplicateCoachSessionQueueHistoryCount > 0;
}
};