diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index 0764efc..ef7641e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -4335,6 +4335,251 @@ FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithQueue return Brief; } +FHyperTwistCoachSignal UHyperTwistTrainingCoachLibrary::DeriveSchedulePolicyFeedbackSignal( + const FHyperTwistCoachSchedulePolicySummary& SchedulePolicySummary, + const FHyperTwistTrainingCoachSessionQueueExecutionSummary& QueueExecutionSummary, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistCoachBrief& CurrentBrief) +{ + if (!SchedulePolicySummary.IsStructurallyValid() || !QueueExecutionSummary.IsStructurallyValid()) + { + return FHyperTwistCoachSignal(); + } + + const bool bManualRescheduleReview = SchedulePolicySummary.bNeedsManualRescheduleReview; + const bool bArchiveAwareDeferrals = SchedulePolicySummary.bHasArchiveAwareDeferrals; + const bool bBlockedFollowUpDeferrals = SchedulePolicySummary.bHasBlockedFollowUpDeferrals; + const bool bOutstandingDeferredWork = QueueExecutionSummary.bHasOutstandingDeferredWork; + const bool bCompletionGap = QueueExecutionSummary.bHasCompletionGap; + const bool bRepeatManualReschedule = QueueExecutionSummary.bHasRepeatManualRescheduleFriction; + const bool bHighPolicyPressure = SchedulePolicySummary.ReschedulePressureScore >= 25.0f; + const bool bHighExecutionPressure = QueueExecutionSummary.ExecutionPressureScore >= 25.0f; + const bool bNeedsFeedback = + bHighPolicyPressure + || bHighExecutionPressure + || bManualRescheduleReview + || bArchiveAwareDeferrals + || bBlockedFollowUpDeferrals + || bOutstandingDeferredWork + || bCompletionGap + || bRepeatManualReschedule; + if (!bNeedsFeedback) + { + return FHyperTwistCoachSignal(); + } + + const bool bHistoryPrefersFollowUp = + SchedulePolicySummary.bFollowUpContinuationFromRecoveryHistory; + const bool bHistoryPrefersRecommended = + SchedulePolicySummary.bBorderlineFollowUpSuppressedByRecoveryHistory; + const bool bPreferFollowUpRecovery = + bBlockedFollowUpDeferrals + || bHistoryPrefersFollowUp + || CoachMemorySnapshot.PreferredGuidanceLane + == EHyperTwistTrainingCoachGuidanceLane::FollowUp; + const bool bPreferRecommendedRecovery = + !bPreferFollowUpRecovery + && ( + bHistoryPrefersRecommended + || CoachMemorySnapshot.PreferredGuidanceLane + == EHyperTwistTrainingCoachGuidanceLane::Recommended + ); + const bool bCompetingGuidancePressure = + CoachMemorySnapshot.RecommendedGuidanceContinueBias > 0 + && CoachMemorySnapshot.FollowUpGuidanceContinueBias > 0; + + const FString SignalId = bPreferFollowUpRecovery + ? TEXT("recover_follow_up_schedule_friction") + : (bPreferRecommendedRecovery + ? TEXT("recover_recommended_schedule_friction") + : (bArchiveAwareDeferrals + ? TEXT("stabilize_archive_aware_schedule") + : (bManualRescheduleReview + ? TEXT("review_manual_schedule_friction") + : (bCompletionGap + ? TEXT("recover_schedule_completion_gap") + : TEXT("recover_schedule_friction"))))); + const FString Headline = + bBlockedFollowUpDeferrals + ? (bCompletionGap + ? TEXT("Live queue friction is still blocking follow-up completion") + : TEXT("Live queue friction is still holding follow-up work back")) + : (bManualRescheduleReview + ? TEXT("Manual reschedule pressure is still shaping the next coaching run") + : (bArchiveAwareDeferrals + ? TEXT("Archive-aware deferrals are still shaping the next coaching run") + : (bCompletionGap + ? TEXT("Live queue execution still needs a tighter completion pass") + : TEXT("Deferred queue work still needs schedule recovery")))); + const FString Detail = FString::Printf( + TEXT("policyPressure=%.2f, executionPressure=%.2f, immediate=%d, pending=%d, deferred=%d, manualRescheduleEntries=%d, manualRescheduleEvents=%d, completionGap=%s, outstandingDeferred=%s, blockedFollowUp=%s, archiveAware=%s, dominantDeferral=%s, closure=%s"), + SchedulePolicySummary.ReschedulePressureScore, + QueueExecutionSummary.ExecutionPressureScore, + SchedulePolicySummary.ImmediateEntryCount, + SchedulePolicySummary.PendingEntryCount, + SchedulePolicySummary.DeferredEntryCount, + SchedulePolicySummary.ManualRescheduleEntryCount, + QueueExecutionSummary.ManualRescheduleEventCount, + bCompletionGap ? TEXT("yes") : TEXT("no"), + bOutstandingDeferredWork ? TEXT("yes") : TEXT("no"), + bBlockedFollowUpDeferrals ? TEXT("yes") : TEXT("no"), + bArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"), + SchedulePolicySummary.DominantDeferralReasonLabel.IsEmpty() + ? TEXT("n/a") + : *SchedulePolicySummary.DominantDeferralReasonLabel, + SchedulePolicySummary.ClosurePolicyLine.IsEmpty() + ? TEXT("n/a") + : *SchedulePolicySummary.ClosurePolicyLine + ); + const float Score = FMath::Clamp( + 59.0f + + FMath::Min(SchedulePolicySummary.ReschedulePressureScore * 0.22f, 18.0f) + + FMath::Min(QueueExecutionSummary.ExecutionPressureScore * 0.18f, 14.0f) + + (bBlockedFollowUpDeferrals ? 7.0f : 0.0f) + + (bManualRescheduleReview ? 5.0f : 0.0f) + + (bArchiveAwareDeferrals ? 4.0f : 0.0f) + + (bOutstandingDeferredWork ? 5.0f : 0.0f) + + (bCompletionGap ? 5.0f : 0.0f) + + (bRepeatManualReschedule ? 4.0f : 0.0f), + 0.0f, + 100.0f + ); + const EHyperTwistCoachFocusLane FocusLane = + bArchiveAwareDeferrals && !bBlockedFollowUpDeferrals + ? EHyperTwistCoachFocusLane::Stability + : EHyperTwistCoachFocusLane::Cadence; + const EHyperTwistTrainingDeliveryMode SuggestedMode = + bPreferFollowUpRecovery + ? EHyperTwistTrainingDeliveryMode::CoachReviewed + : ((CurrentBrief.UserId == SchedulePolicySummary.UserId + && CurrentBrief.SuggestedMode != EHyperTwistTrainingDeliveryMode::Timer) + ? CurrentBrief.SuggestedMode + : CoachMemorySnapshot.DominantCoachSuggestedMode); + const EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = + bPreferFollowUpRecovery + ? EHyperTwistTrainingSelectionPolicy::Scripted + : ((bCompletionGap || bOutstandingDeferredWork) + ? EHyperTwistTrainingSelectionPolicy::Weighted + : ((CurrentBrief.UserId == SchedulePolicySummary.UserId + && CurrentBrief.SuggestedSelectionPolicy + != EHyperTwistTrainingSelectionPolicy::Weighted) + ? CurrentBrief.SuggestedSelectionPolicy + : CoachMemorySnapshot.DominantCoachSelectionPolicy)); + + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + SignalId, + FocusLane, + Score, + Headline, + Detail, + SuggestedMode, + SuggestedSelectionPolicy + ); + Signal.SuggestedDeckId = !CurrentBrief.FocusDeckId.IsEmpty() + ? CurrentBrief.FocusDeckId + : SchedulePolicySummary.FocusDeckId; + Signal.SuggestedMethodSegmentId = !CurrentBrief.FocusMethodSegmentId.IsEmpty() + ? CurrentBrief.FocusMethodSegmentId + : SchedulePolicySummary.FocusMethodSegmentId; + Signal.SuggestedCaseIds = CurrentBrief.FocusCaseIds.Num() > 0 + ? CurrentBrief.FocusCaseIds + : (CoachMemorySnapshot.SchedulePressureCaseIds.Num() > 0 + ? CoachMemorySnapshot.SchedulePressureCaseIds + : CoachMemorySnapshot.DueCaseIds); + HyperTwistTrainingCoachLibraryInternal::ApplyGuidancePreferenceToSignal( + Signal, + bPreferFollowUpRecovery + ? EHyperTwistTrainingCoachGuidanceLane::FollowUp + : (bPreferRecommendedRecovery + ? EHyperTwistTrainingCoachGuidanceLane::Recommended + : CoachMemorySnapshot.PreferredGuidanceLane), + CoachMemorySnapshot, + bCompetingGuidancePressure + ); + return Signal; +} + +FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithSchedulePolicyFeedbackSignal( + const FHyperTwistCoachBrief& BaseBrief, + const FHyperTwistCoachSignal& SchedulePolicyFeedbackSignal) +{ + if (SchedulePolicyFeedbackSignal.SignalId.IsEmpty()) + { + return BaseBrief; + } + + FHyperTwistCoachBrief Brief = BaseBrief; + if (!Brief.Signals.ContainsByPredicate( + [&SchedulePolicyFeedbackSignal](const FHyperTwistCoachSignal& Candidate) + { + return Candidate.SignalId == SchedulePolicyFeedbackSignal.SignalId; + })) + { + Brief.Signals.Add(SchedulePolicyFeedbackSignal); + } + HyperTwistTrainingCoachLibraryInternal::SortSignals(Brief.Signals); + + const bool bPolicyFacingBrief = + Brief.FocusLane == EHyperTwistCoachFocusLane::Cadence + || Brief.FocusLane == EHyperTwistCoachFocusLane::Stability + || Brief.bNeedsCadenceRecovery; + const bool bShouldPromotePolicySignal = + bPolicyFacingBrief + && SchedulePolicyFeedbackSignal.Score >= Brief.PriorityScore - 3.5f; + if (bShouldPromotePolicySignal) + { + for (FHyperTwistCoachSignal& Signal : Brief.Signals) + { + if (Signal.SignalId == SchedulePolicyFeedbackSignal.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 OriginalCaseIds = Brief.FocusCaseIds; + const FString OriginalPrimaryActionLabel = Brief.PrimaryActionLabel; + 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; + } + if (bShouldPromotePolicySignal + && Brief.PrimaryActionLabel == SchedulePolicyFeedbackSignal.SignalId + && (!OriginalPrimaryActionLabel.IsEmpty() + && (OriginalPrimaryActionLabel.Contains(TEXT("schedule")) + || OriginalPrimaryActionLabel.Contains(TEXT("recovery")) + || OriginalPrimaryActionLabel.Contains(TEXT("promote_deferred")) + || OriginalPrimaryActionLabel.Contains(TEXT("reschedule")) + || OriginalPrimaryActionLabel.Contains(TEXT("archive"))))) + { + Brief.PrimaryActionLabel = OriginalPrimaryActionLabel; + } + } + + return Brief; +} + FHyperTwistCoachSignal UHyperTwistTrainingCoachLibrary::DeriveClosureRecoveryHistorySignal( const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary, const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 38f1b56..2a364f9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -4143,6 +4143,21 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachSessionQueueState.QueueId, ActiveLearnerDeckStateSummary.ReferenceUtc ); + const FHyperTwistCoachSignal SchedulePolicyFeedbackSignal = + UHyperTwistTrainingCoachLibrary::DeriveSchedulePolicyFeedbackSignal( + ActiveCoachSchedulePolicySummary, + ActiveCoachSessionQueueExecutionSummary, + ActiveCoachMemorySnapshot, + ActiveCoachBrief + ); + if (!SchedulePolicyFeedbackSignal.SignalId.IsEmpty()) + { + ActiveCoachBrief = UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithSchedulePolicyFeedbackSignal( + ActiveCoachBrief, + SchedulePolicyFeedbackSignal + ); + ActiveCoachSignals = ActiveCoachBrief.Signals; + } } else { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h index 1f51729..a319b40 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -487,6 +487,20 @@ public: const FHyperTwistCoachSignal& QueueSuppressionSignal ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachSignal DeriveSchedulePolicyFeedbackSignal( + const FHyperTwistCoachSchedulePolicySummary& SchedulePolicySummary, + const FHyperTwistTrainingCoachSessionQueueExecutionSummary& QueueExecutionSummary, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistCoachBrief& CurrentBrief + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachBrief RefineCoachBriefWithSchedulePolicyFeedbackSignal( + const FHyperTwistCoachBrief& BaseBrief, + const FHyperTwistCoachSignal& SchedulePolicyFeedbackSignal + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") static FHyperTwistCoachSignal DeriveClosureRecoveryHistorySignal( const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary, diff --git a/docs/arch/HYPERTWIST_PHASE3_SCHEDULE_POLICY_FEEDBACK_CLOSURE_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE3_SCHEDULE_POLICY_FEEDBACK_CLOSURE_PACKET_2026-05-06.md new file mode 100644 index 0000000..cf7618f --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE3_SCHEDULE_POLICY_FEEDBACK_CLOSURE_PACKET_2026-05-06.md @@ -0,0 +1,91 @@ +# HyperTwist Phase 3 schedule-policy feedback closure packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded coaching-stack closure slice + +## Purpose + +This packet defines the next logical Phase 3 closure task after the linked-state rebuild closure packet landed. + +The open task is: + +- fold resolved live coach queue schedule-policy and execution summaries back into coach brief shaping +- close the remaining one-way gap where current queue policy/execution truth is derived, surfaced, and persisted, but does not directly refine the active coach brief in the same refresh pass + +It is not: + +- a new queue model +- a broad coaching heuristic rewrite +- a dashboard flow redesign +- a donor widening task + +## Scope + +Bounded lane: + +- final first-party schedule-friction feedback closure between resolved coach queue state and coach brief shaping + +Required result: + +- the subsystem derives the current coach brief first, resolves queue state from that brief, derives live schedule-policy and queue-execution summaries, and then runs a second-pass coach brief refinement from those resolved summaries +- the feedback signal uses current first-party queue policy/execution truth, not only previously persisted memory-derived friction fields +- the resulting brief keeps existing launch/recovery action labels when they are already more specific than the feedback signal id + +Out of scope: + +- rebuilding queue state more than once per refresh +- changing queue creation heuristics +- changing generated-mode executor behavior +- changing external plugin copies + +## Current owned product truth + +These pieces are already live in first-party code: + +- coach memory snapshot derivation from repository state +- coach brief derivation from learner/review/coach-memory inputs +- queue summary, queue state, queue horizon, schedule policy, and queue execution derivation +- dashboard surfaces for queue policy/execution truth + +What was still missing: + +- a same-pass feedback loop from the resolved live queue schedule/execution summaries back into the active coach brief +- explicit live schedule-policy pressure participating in coach signal ordering after queue state resolution + +## Implementation target + +The slice should do the following: + +1. derive a live schedule-policy feedback signal from: + - `FHyperTwistCoachSchedulePolicySummary` + - `FHyperTwistTrainingCoachSessionQueueExecutionSummary` + - current coach memory preference state + - current coach brief +2. refine the active coach brief with that signal after queue state, horizon, policy, and execution summaries are resolved +3. preserve existing launch/recovery labels when the prior brief was already carrying a more specific schedule-facing action label + +## Suggested file ownership + +Primary edit surfaces: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp` + +## Acceptance criteria + +- a new second-pass schedule-policy feedback signal is available in first-party coach derivation +- `RefreshRepositoryViews()` refines `ActiveCoachBrief` from resolved queue policy/execution summaries in the same refresh pass +- the full UE build still succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` +2. confirm the new signal compiles through coach library and subsystem consumers +3. confirm the active coach brief now receives live schedule-policy / execution feedback after queue state resolution + +That is the packet.