Explain closure recovery through coach brief

This commit is contained in:
axiomlogicnexus 2026-04-27 20:35:20 +02:00
parent 457d77b334
commit c8c879dc7e
3 changed files with 200 additions and 0 deletions

View file

@ -1494,6 +1494,179 @@ FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithQueue
return Brief;
}
FHyperTwistCoachSignal UHyperTwistTrainingCoachLibrary::DeriveClosureRecoveryHistorySignal(
const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,
const FHyperTwistCoachBrief& CurrentBrief)
{
if (!ClosureSummary.IsStructurallyValid())
{
return FHyperTwistCoachSignal();
}
const int32 SustainedHistoryCount = CoachMemorySnapshot.RecoverySustainedFollowUpHistoryCount;
const int32 SuppressedHistoryCount =
CoachMemorySnapshot.RecoverySuppressedBorderlineFollowUpHistoryCount;
if (SustainedHistoryCount <= 0 && SuppressedHistoryCount <= 0)
{
return FHyperTwistCoachSignal();
}
const bool bHistoryPrefersFollowUp = SustainedHistoryCount > SuppressedHistoryCount;
const bool bHistoryPrefersRecommended = SuppressedHistoryCount > SustainedHistoryCount;
if (!bHistoryPrefersFollowUp && !bHistoryPrefersRecommended)
{
return FHyperTwistCoachSignal();
}
const bool bCurrentFollowUpContinuation =
ClosureSummary.bSpawnFollowUpFromSuppressionMemory;
const bool bCurrentBorderlineSuppression =
ClosureSummary.bSuppressedBorderlineFollowUpByRecoveryHistory;
const bool bCompetingGuidancePressure =
CoachMemorySnapshot.RecommendedGuidanceContinueBias > 0
&& CoachMemorySnapshot.FollowUpGuidanceContinueBias > 0;
const float Score = FMath::Clamp(
(bHistoryPrefersFollowUp ? 64.0f : 60.0f)
+ static_cast<float>(FMath::Max(SustainedHistoryCount, SuppressedHistoryCount)) * 4.0f
+ (bCurrentFollowUpContinuation || bCurrentBorderlineSuppression ? 6.0f : 0.0f)
+ (bCompetingGuidancePressure ? 4.0f : 0.0f),
0.0f,
100.0f
);
const FString SignalId = bHistoryPrefersFollowUp
? TEXT("closure_history_follow_up_continuation")
: TEXT("closure_history_recommended_suppression");
const EHyperTwistCoachFocusLane FocusLane = bHistoryPrefersFollowUp
? EHyperTwistCoachFocusLane::Cadence
: EHyperTwistCoachFocusLane::Stability;
const FString Headline = bHistoryPrefersFollowUp
? (bCurrentFollowUpContinuation
? TEXT("Recovery history keeps follow-up continuity alive")
: TEXT("Recovery history favors sustained follow-up continuity"))
: (bCurrentBorderlineSuppression
? TEXT("Recovery history suppresses borderline follow-up regeneration")
: TEXT("Recovery history favors recommended consolidation over borderline follow-up"));
const FString Detail = FString::Printf(
TEXT("keepHistory=%d, suppressHistory=%d, currentClosure=%s, followUpReady=%d, recoverySource=%s"),
SustainedHistoryCount,
SuppressedHistoryCount,
ClosureSummary.ClosureLabel.IsEmpty() ? TEXT("n/a") : *ClosureSummary.ClosureLabel,
CoachMemorySnapshot.FollowUpReadyCoachSequenceCount,
CoachMemorySnapshot.PreferredGuidanceSourceLabel.IsEmpty()
? TEXT("n/a")
: *CoachMemorySnapshot.PreferredGuidanceSourceLabel
);
const EHyperTwistTrainingDeliveryMode SuggestedMode = bHistoryPrefersFollowUp
? EHyperTwistTrainingDeliveryMode::CoachReviewed
: CoachMemorySnapshot.DominantCoachSuggestedMode;
const EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = bHistoryPrefersFollowUp
? EHyperTwistTrainingSelectionPolicy::Scripted
: CoachMemorySnapshot.DominantCoachSelectionPolicy;
FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal(
SignalId,
FocusLane,
Score,
Headline,
Detail,
SuggestedMode,
SuggestedSelectionPolicy
);
Signal.SuggestedDeckId = !CurrentBrief.FocusDeckId.IsEmpty()
? CurrentBrief.FocusDeckId
: CoachMemorySnapshot.FocusDeckId;
Signal.SuggestedMethodSegmentId = !CurrentBrief.FocusMethodSegmentId.IsEmpty()
? CurrentBrief.FocusMethodSegmentId
: CoachMemorySnapshot.FocusMethodSegmentId;
Signal.SuggestedCaseIds = bHistoryPrefersFollowUp
? (CoachMemorySnapshot.SchedulePressureCaseIds.Num() > 0
? CoachMemorySnapshot.SchedulePressureCaseIds
: CoachMemorySnapshot.PersistentCoachWeakCaseIds)
: (CoachMemorySnapshot.RecurringCoachCaseIds.Num() > 0
? CoachMemorySnapshot.RecurringCoachCaseIds
: CoachMemorySnapshot.DueCaseIds);
HyperTwistTrainingCoachLibraryInternal::ApplyGuidancePreferenceToSignal(
Signal,
bHistoryPrefersFollowUp
? EHyperTwistTrainingCoachGuidanceLane::FollowUp
: EHyperTwistTrainingCoachGuidanceLane::Recommended,
CoachMemorySnapshot,
bCompetingGuidancePressure
);
return Signal;
}
FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithClosureRecoveryHistorySignal(
const FHyperTwistCoachBrief& BaseBrief,
const FHyperTwistCoachSignal& ClosureRecoveryHistorySignal)
{
if (ClosureRecoveryHistorySignal.SignalId.IsEmpty())
{
return BaseBrief;
}
FHyperTwistCoachBrief Brief = BaseBrief;
if (!Brief.Signals.ContainsByPredicate(
[&ClosureRecoveryHistorySignal](const FHyperTwistCoachSignal& Candidate)
{
return Candidate.SignalId == ClosureRecoveryHistorySignal.SignalId;
}))
{
Brief.Signals.Add(ClosureRecoveryHistorySignal);
}
HyperTwistTrainingCoachLibraryInternal::SortSignals(Brief.Signals);
const bool bPolicyFacingBrief =
Brief.FocusLane == EHyperTwistCoachFocusLane::Cadence
|| Brief.FocusLane == EHyperTwistCoachFocusLane::Stability
|| Brief.bNeedsCadenceRecovery;
const bool bShouldPromoteHistorySignal =
bPolicyFacingBrief
&& ClosureRecoveryHistorySignal.Score >= Brief.PriorityScore - 5.0f;
if (bShouldPromoteHistorySignal)
{
for (FHyperTwistCoachSignal& Signal : Brief.Signals)
{
if (Signal.SignalId == ClosureRecoveryHistorySignal.SignalId)
{
Signal.Score = FMath::Clamp(
FMath::Max(Signal.Score, Brief.Signals[0].Score + 0.5f),
0.0f,
100.0f
);
Signal.Priority = HyperTwistTrainingCoachLibraryInternal::PriorityFromScore(Signal.Score);
Signal.bNeedsImmediateAttention = Signal.Priority == EHyperTwistCoachPriority::Critical;
break;
}
}
HyperTwistTrainingCoachLibraryInternal::SortSignals(Brief.Signals);
}
if (Brief.Signals.Num() > 0)
{
const FString OriginalDeckId = Brief.FocusDeckId;
const FString OriginalMethodSegmentId = Brief.FocusMethodSegmentId;
const TArray<FString> OriginalCaseIds = Brief.FocusCaseIds;
HyperTwistTrainingCoachLibraryInternal::ApplyPrimarySignalToBrief(Brief, Brief.Signals[0]);
if (Brief.FocusDeckId.IsEmpty())
{
Brief.FocusDeckId = OriginalDeckId;
}
if (Brief.FocusMethodSegmentId.IsEmpty())
{
Brief.FocusMethodSegmentId = OriginalMethodSegmentId;
}
if (Brief.FocusCaseIds.Num() <= 0)
{
Brief.FocusCaseIds = OriginalCaseIds;
}
}
return Brief;
}
FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingCoachLibrary::DeriveCoachScheduleHorizonSummary(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,

View file

@ -2229,6 +2229,20 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveReplayReviewAnalytics,
ActiveRecognitionReplaySummary
);
const FHyperTwistCoachSignal ClosureRecoveryHistorySignal =
UHyperTwistTrainingCoachLibrary::DeriveClosureRecoveryHistorySignal(
ActiveCoachActionClosureSummary,
ActiveCoachMemorySnapshot,
ActiveCoachBrief
);
if (!ClosureRecoveryHistorySignal.SignalId.IsEmpty())
{
ActiveCoachBrief = UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithClosureRecoveryHistorySignal(
ActiveCoachBrief,
ClosureRecoveryHistorySignal
);
ActiveCoachSignals = ActiveCoachBrief.Signals;
}
ActiveCoachFollowUpBrief = ActiveCoachActionPlanOutcomeSummary.IsStructurallyValid()
&& (
ActiveCoachActionClosureSummary.bShouldSpawnFollowUp

View file

@ -478,6 +478,19 @@ public:
const FHyperTwistCoachSignal& QueueSuppressionSignal
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSignal DeriveClosureRecoveryHistorySignal(
const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,
const FHyperTwistCoachBrief& CurrentBrief
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachBrief RefineCoachBriefWithClosureRecoveryHistorySignal(
const FHyperTwistCoachBrief& BaseBrief,
const FHyperTwistCoachSignal& ClosureRecoveryHistorySignal
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachScheduleHorizonSummary DeriveCoachScheduleHorizonSummary(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,