Add coach scheduling policy controls

This commit is contained in:
axiomlogicnexus 2026-04-26 04:32:38 +02:00
parent 5ec0257f1f
commit 1a309c862c
11 changed files with 564 additions and 26 deletions

View file

@ -1323,11 +1323,29 @@ FHyperTwistCoachSessionQueueSummary UHyperTwistContractLibrary::MakeSampleTraini
FHyperTwistTrainingCoachSessionQueueState UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueState()
{
return UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
FHyperTwistTrainingCoachSessionQueueState QueueState = UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
MakeSampleTrainingCoachSessionQueueSummary(),
TEXT("coach_queue_active"),
TEXT("2026-04-28T12:12:00Z")
);
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")
);
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")
);
return QueueState;
}
FHyperTwistCoachScheduleHorizonSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachScheduleHorizonSummary()
@ -1339,6 +1357,16 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistContractLibrary::MakeSampleTra
);
}
FHyperTwistCoachSchedulePolicySummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSchedulePolicySummary()
{
return UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary(
MakeSampleTrainingCoachSessionQueueState(),
MakeSampleTrainingCoachScheduleHorizonSummary(),
MakeSampleTrainingCoachArchivePolicySummary(),
TEXT("2026-04-28T12:30:00Z")
);
}
bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State)
{
return State.IsStructurallyValid();

View file

@ -746,7 +746,9 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingCoachLibrary::DeriveCo
const bool bIsImmediateEntry =
Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress;
const bool bIsDeferredEntry = Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
const bool bIsDeferredEntry =
Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
|| Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
Summary.ImmediateEntryCount += bIsImmediateEntry ? 1 : 0;
Summary.DeferredEntryCount += bIsDeferredEntry ? 1 : 0;
@ -808,3 +810,124 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingCoachLibrary::DeriveCo
return Summary;
}
FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FHyperTwistCoachScheduleHorizonSummary& HorizonSummary,
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,
const FString& ReferenceUtc
)
{
FHyperTwistCoachSchedulePolicySummary Summary;
if (!QueueState.IsStructurallyValid())
{
return Summary;
}
Summary.UserId = QueueState.UserId;
Summary.ReferenceUtc = !ReferenceUtc.IsEmpty()
? ReferenceUtc
: (!QueueState.LastUpdatedAtUtc.IsEmpty() ? QueueState.LastUpdatedAtUtc : QueueState.ReferenceUtc);
Summary.QueueId = QueueState.QueueId;
Summary.FocusDeckId = QueueState.FocusDeckId;
Summary.FocusMethodSegmentId = QueueState.FocusMethodSegmentId;
Summary.NextImmediateEntryId = HorizonSummary.NextReadyEntryId;
TMap<FString, int32> DeferralReasonCounts;
for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries)
{
if (!Entry.IsStructurallyValid())
{
continue;
}
switch (Entry.EntryState)
{
case EHyperTwistTrainingCoachSessionQueueEntryState::Ready:
case EHyperTwistTrainingCoachSessionQueueEntryState::InProgress:
Summary.ImmediateEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Pending:
Summary.PendingEntryCount += 1;
if (Summary.NextPendingEntryId.IsEmpty())
{
Summary.NextPendingEntryId = Entry.EntryId;
}
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Deferred:
Summary.DeferredEntryCount += 1;
if (Summary.NextDeferredEntryId.IsEmpty())
{
Summary.NextDeferredEntryId = Entry.EntryId;
}
if (Entry.bRequiresArchiveRetentionTuning)
{
Summary.ArchiveAwareDeferredEntryCount += 1;
}
if (Entry.bRequiresFollowUp)
{
Summary.BlockedFollowUpDeferredEntryCount += 1;
}
if (Summary.EarliestDeferredUntilUtc.IsEmpty()
|| (!Entry.DeferredUntilUtc.IsEmpty() && Entry.DeferredUntilUtc < Summary.EarliestDeferredUntilUtc))
{
Summary.EarliestDeferredUntilUtc = Entry.DeferredUntilUtc;
}
if (!Entry.DeferralReasonLabel.IsEmpty())
{
DeferralReasonCounts.FindOrAdd(Entry.DeferralReasonLabel) += 1;
}
if (!Entry.DeferredUntilUtc.IsEmpty() && Entry.DeferredUntilUtc <= Summary.ReferenceUtc)
{
Summary.bHasExpiredDeferrals = true;
}
break;
default:
break;
}
if (Entry.bWasManuallyRescheduled)
{
Summary.ManualRescheduleEntryCount += 1;
if (Summary.NextManualRescheduleEntryId.IsEmpty()
&& (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
|| Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending))
{
Summary.NextManualRescheduleEntryId = Entry.EntryId;
}
}
}
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;
}
}
Summary.bAllowsManualReschedule = QueueState.IsStructurallyValid()
&& !HorizonSummary.bHasInProgressEntry;
Summary.bHasArchiveAwareDeferrals = Summary.ArchiveAwareDeferredEntryCount > 0
|| (ArchivePolicySummary.bNeedsArchiveRetentionTuning && Summary.DeferredEntryCount > 0);
Summary.bHasBlockedFollowUpDeferrals = Summary.BlockedFollowUpDeferredEntryCount > 0;
Summary.bNeedsManualRescheduleReview = Summary.bHasExpiredDeferrals
|| Summary.ManualRescheduleEntryCount >= 2
|| Summary.bHasBlockedFollowUpDeferrals;
Summary.ReschedulePressureScore = FMath::Clamp(
static_cast<float>(Summary.DeferredEntryCount) * 8.0f
+ static_cast<float>(Summary.ManualRescheduleEntryCount) * 10.0f
+ static_cast<float>(Summary.ArchiveAwareDeferredEntryCount) * 12.0f
+ static_cast<float>(Summary.BlockedFollowUpDeferredEntryCount) * 12.0f
+ (Summary.bHasExpiredDeferrals ? 15.0f : 0.0f),
0.0f,
100.0f
);
return Summary;
}

View file

@ -487,6 +487,18 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
|| EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired;
}
bool IsUtcDueOrPast(const FString& CandidateUtc, const FString& ReferenceUtc)
{
FDateTime CandidateTime;
FDateTime ReferenceTime;
if (!TryParseUtc(CandidateUtc, CandidateTime) || !TryParseUtc(ReferenceUtc, ReferenceTime))
{
return false;
}
return CandidateTime <= ReferenceTime;
}
FString MakeCoachSessionQueueEntryFingerprint(
const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry
)
@ -531,9 +543,20 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
{
NormalizedEntry.WorkFingerprint = MakeCoachSessionQueueEntryFingerprint(NormalizedEntry);
}
NormalizedEntry.RescheduleCount = FMath::Max(0, NormalizedEntry.RescheduleCount);
NormalizedEntry.bPinned = NormalizedEntry.bPinned
|| NormalizedEntry.bRequiresFollowUp
|| NormalizedEntry.bRequiresArchiveRetentionTuning;
if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
&& NormalizedEntry.DeferredUntilUtc.IsEmpty())
{
NormalizedEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
}
if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
&& IsUtcDueOrPast(NormalizedEntry.DeferredUntilUtc, NormalizedState.ReferenceUtc))
{
NormalizedEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
}
if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
@ -541,7 +564,8 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
? NormalizedEntry.ScheduledAtUtc
: NormalizedState.ReferenceUtc;
}
if (IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState))
if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
|| IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState))
{
NormalizedEntry.bCanStartImmediately = false;
}
@ -580,6 +604,7 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
NormalizedState.ActiveEntryId.Reset();
NormalizedState.ActiveTrainingSessionId.Reset();
NormalizedState.PendingEntryCount = 0;
NormalizedState.DeferredEntryCount = 0;
NormalizedState.ReadyEntryCount = 0;
NormalizedState.InProgressEntryCount = 0;
NormalizedState.CompletedEntryCount = 0;
@ -632,6 +657,9 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
case EHyperTwistTrainingCoachSessionQueueEntryState::Pending:
NormalizedState.PendingEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Deferred:
NormalizedState.DeferredEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Ready:
NormalizedState.ReadyEntryCount += 1;
break;
@ -656,30 +684,9 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
{
for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : NormalizedEntries)
{
if (!IsTerminalCoachQueueEntryState(Entry.EntryState))
if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending)
{
if (Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Pending)
{
switch (Entry.EntryState)
{
case EHyperTwistTrainingCoachSessionQueueEntryState::Completed:
NormalizedState.CompletedEntryCount -= 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Skipped:
NormalizedState.SkippedEntryCount -= 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Expired:
NormalizedState.ExpiredEntryCount -= 1;
break;
default:
break;
}
}
else
{
NormalizedState.PendingEntryCount -= 1;
}
NormalizedState.PendingEntryCount -= 1;
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready;
Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc;
Entry.bCanStartImmediately = true;
@ -5398,10 +5405,15 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::
QueueEntry.EntryState = ExistingEntry->EntryState;
QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId;
QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc;
QueueEntry.DeferredUntilUtc = ExistingEntry->DeferredUntilUtc;
QueueEntry.DeferredAtUtc = ExistingEntry->DeferredAtUtc;
QueueEntry.DeferralReasonLabel = ExistingEntry->DeferralReasonLabel;
QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc;
QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc;
QueueEntry.RescheduleCount = ExistingEntry->RescheduleCount;
QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned;
QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately;
QueueEntry.bWasManuallyRescheduled = ExistingEntry->bWasManuallyRescheduled;
}
ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
@ -5431,6 +5443,7 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::
{
const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex];
if (Entry.SourceLabel == SourceLabel
&& Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
&& !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState))
{
ActivatedIndex = EntryIndex;
@ -5460,6 +5473,7 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::
ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty()
? ActiveEntry.ScheduledAtUtc
: ResolvedStartedAtUtc;
ActiveEntry.DeferredUntilUtc.Reset();
ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc;
ActiveEntry.bCanStartImmediately = false;
UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc;
@ -5513,6 +5527,98 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty())
{
return UpdatedQueueState;
}
const FString ResolvedReferenceUtc =
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc
);
FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToDefer =
UpdatedQueueState.Entries.FindByPredicate(
[&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == EntryId
&& Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress
&& !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState);
}
);
if (EntryToDefer == nullptr)
{
return UpdatedQueueState;
}
EntryToDefer->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Deferred;
EntryToDefer->DeferredUntilUtc = DeferredUntilUtc;
EntryToDefer->DeferredAtUtc = ResolvedReferenceUtc;
EntryToDefer->DeferralReasonLabel = !DeferralReasonLabel.IsEmpty()
? DeferralReasonLabel
: (bManualReschedule ? TEXT("manual-reschedule") : TEXT("deferred-window"));
EntryToDefer->SourceTrainingSessionId.Reset();
EntryToDefer->StartedAtUtc.Reset();
EntryToDefer->CompletedAtUtc.Reset();
EntryToDefer->bCanStartImmediately = false;
EntryToDefer->RescheduleCount += 1;
EntryToDefer->bWasManuallyRescheduled = EntryToDefer->bWasManuallyRescheduled || bManualReschedule;
UpdatedQueueState.ActiveEntryId.Reset();
UpdatedQueueState.ActiveTrainingSessionId.Reset();
UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc;
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty())
{
return UpdatedQueueState;
}
const FString ResolvedReferenceUtc =
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc
);
FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToPromote =
UpdatedQueueState.Entries.FindByPredicate(
[&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == EntryId
&& Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress
&& !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState);
}
);
if (EntryToPromote == nullptr)
{
return UpdatedQueueState;
}
EntryToPromote->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready;
EntryToPromote->ScheduledAtUtc = ResolvedReferenceUtc;
EntryToPromote->DeferredUntilUtc.Reset();
EntryToPromote->bCanStartImmediately = true;
EntryToPromote->SlotIndex = -1;
UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc;
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
TArray<FHyperTwistTrainingCoachSessionQueueState> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId

View file

@ -393,6 +393,18 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingRuntimeLibrary::GetAct
return FHyperTwistCoachScheduleHorizonSummary();
}
FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSchedulePolicySummary(
UObject* WorldContextObject
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveCoachSchedulePolicySummary();
}
return FHyperTwistCoachSchedulePolicySummary();
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
@ -697,6 +709,40 @@ bool UHyperTwistTrainingRuntimeLibrary::SaveCoachSessionQueueState(
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::DeferCoachSessionQueueEntry(
UObject* WorldContextObject,
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->DeferCoachSessionQueueEntry(
EntryId,
DeferredUntilUtc,
DeferralReasonLabel,
bManualReschedule
);
}
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::PromoteCoachSessionQueueEntry(
UObject* WorldContextObject,
const FString& EntryId
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->PromoteCoachSessionQueueEntry(EntryId);
}
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::RemoveMethodDrillBatch(
UObject* WorldContextObject,
const FString& UserId,

View file

@ -178,6 +178,11 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingSubsystem::GetActiveCo
return ActiveCoachScheduleHorizonSummary;
}
FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingSubsystem::GetActiveCoachSchedulePolicySummary() const
{
return ActiveCoachSchedulePolicySummary;
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const
{
return ActiveLearnerMethodSegments;
@ -445,6 +450,64 @@ bool UHyperTwistTrainingSubsystem::SaveCoachSessionQueueState(
return true;
}
bool UHyperTwistTrainingSubsystem::DeferCoachSessionQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule
)
{
if (!ActiveCoachSessionQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty())
{
return false;
}
ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry(
ActiveCoachSessionQueueState,
EntryId,
DeferredUntilUtc,
DeferralReasonLabel,
bManualReschedule,
FDateTime::UtcNow().ToIso8601()
);
if (!ActiveCoachSessionQueueState.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
ActiveCoachSessionQueueState
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::PromoteCoachSessionQueueEntry(const FString& EntryId)
{
if (!ActiveCoachSessionQueueState.IsStructurallyValid() || EntryId.IsEmpty())
{
return false;
}
ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry(
ActiveCoachSessionQueueState,
EntryId,
FDateTime::UtcNow().ToIso8601()
);
if (!ActiveCoachSessionQueueState.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
ActiveCoachSessionQueueState
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::RemoveMethodDrillBatch(
const FString& UserId,
const FString& BatchId
@ -1321,6 +1384,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachSessionQueueSummary = FHyperTwistCoachSessionQueueSummary();
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary();
ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary();
ActiveLearnerMethodSegments.Reset();
ActiveLearnerPresets.Reset();
ActiveTrainingSessionTemplates.Reset();
@ -1528,11 +1592,18 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachArchivePolicySummary,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
ActiveCoachSchedulePolicySummary = UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary(
ActiveCoachSessionQueueState,
ActiveCoachScheduleHorizonSummary,
ActiveCoachArchivePolicySummary,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
}
else
{
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary();
ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary();
}
}

View file

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

View file

@ -311,6 +311,86 @@ struct FHyperTwistCoachScheduleHorizonSummary
}
};
USTRUCT(BlueprintType)
struct FHyperTwistCoachSchedulePolicySummary
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReferenceUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString QueueId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusDeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusMethodSegmentId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextImmediateEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextPendingEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextDeferredEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextManualRescheduleEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EarliestDeferredUntilUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DominantDeferralReasonLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ImmediateEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 PendingEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DeferredEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ManualRescheduleEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ArchiveAwareDeferredEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 BlockedFollowUpDeferredEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float ReschedulePressureScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bAllowsManualReschedule = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasExpiredDeferrals = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bNeedsManualRescheduleReview = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasArchiveAwareDeferrals = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasBlockedFollowUpDeferrals = false;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && !QueueId.IsEmpty();
}
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistTrainingCoachLibrary : public UBlueprintFunctionLibrary
{
@ -362,4 +442,12 @@ public:
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSchedulePolicySummary DeriveCoachSchedulePolicySummary(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FHyperTwistCoachScheduleHorizonSummary& HorizonSummary,
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,
const FString& ReferenceUtc
);
};

View file

@ -377,6 +377,23 @@ public:
bool bAbortEntry
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState DeferCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState PromoteCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static TArray<FHyperTwistTrainingCoachSessionQueueState> ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,

View file

@ -130,6 +130,11 @@ public:
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistCoachSchedulePolicySummary GetActiveTrainingCoachSchedulePolicySummary(
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static TArray<FHyperTwistTrainingMethodSegment> GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject);
@ -266,6 +271,21 @@ public:
const FHyperTwistTrainingCoachSessionQueueState& QueueState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static bool DeferCoachSessionQueueEntry(
UObject* WorldContextObject,
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static bool PromoteCoachSessionQueueEntry(
UObject* WorldContextObject,
const FString& EntryId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject"))
static bool RemoveMethodDrillBatch(
UObject* WorldContextObject,

View file

@ -113,6 +113,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistCoachScheduleHorizonSummary GetActiveCoachScheduleHorizonSummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistCoachSchedulePolicySummary GetActiveCoachSchedulePolicySummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
TArray<FHyperTwistTrainingMethodSegment> GetActiveLearnerMethodSegments() const;
@ -225,6 +228,17 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool SaveCoachSessionQueueState(const FHyperTwistTrainingCoachSessionQueueState& QueueState);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool DeferCoachSessionQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,
const FString& DeferralReasonLabel,
bool bManualReschedule
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool PromoteCoachSessionQueueEntry(const FString& EntryId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
bool RemoveMethodDrillBatch(
const FString& UserId,
@ -481,6 +495,9 @@ private:
UPROPERTY()
FHyperTwistCoachScheduleHorizonSummary ActiveCoachScheduleHorizonSummary;
UPROPERTY()
FHyperTwistCoachSchedulePolicySummary ActiveCoachSchedulePolicySummary;
UPROPERTY()
TArray<FHyperTwistTrainingMethodSegment> ActiveLearnerMethodSegments;

View file

@ -2722,6 +2722,7 @@ UENUM(BlueprintType)
enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8
{
Pending UMETA(DisplayName = "Pending"),
Deferred UMETA(DisplayName = "Deferred"),
Ready UMETA(DisplayName = "Ready"),
InProgress UMETA(DisplayName = "In Progress"),
Completed UMETA(DisplayName = "Completed"),
@ -2921,12 +2922,24 @@ struct FHyperTwistTrainingCoachSessionQueueStateEntry
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ScheduledAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeferredUntilUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeferredAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeferralReasonLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString StartedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CompletedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 RescheduleCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresFollowUp = false;
@ -2939,6 +2952,9 @@ struct FHyperTwistTrainingCoachSessionQueueStateEntry
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCanStartImmediately = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bWasManuallyRescheduled = false;
bool IsStructurallyValid() const
{
return !EntryId.IsEmpty() && !Headline.IsEmpty() && !WorkFingerprint.IsEmpty();
@ -2989,6 +3005,9 @@ struct FHyperTwistTrainingCoachSessionQueueState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 PendingEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DeferredEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ReadyEntryCount = 0;