diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index c658cf6..afbf384 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -1,5 +1,7 @@ #include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h" + namespace HyperTwistTrainingCoachLibraryInternal { EHyperTwistCoachPriority PriorityFromScore(const float Score); @@ -7407,3 +7409,177 @@ UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionMenuSurfa return Surface; } + +FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface +UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionActionabilitySurface( + const FHyperTwistTrainingDeck& ActiveDeck, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface +) +{ + FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface Surface; + Surface.SelectorId = SelectorId; + Surface.SelectorLabel = OptionMenuSurface.SelectorLabel; + Surface.BindingLabel = OptionMenuSurface.BindingLabel; + Surface.Headline = TEXT("Generated Mode Selector Option Coverage"); + Surface.bHasSelector = OptionMenuSurface.bHasSelector; + + if (!SelectorId.IsEmpty()) + { + Surface.Headline = OptionMenuSurface.SelectorLabel.IsEmpty() + ? FString::Printf(TEXT("Generated Mode Selector Option Coverage: %s"), *SelectorId) + : FString::Printf( + TEXT("Generated Mode Selector Option Coverage: %s"), + *OptionMenuSurface.SelectorLabel + ); + } + + if (SelectorId.IsEmpty()) + { + Surface.StatusLine = + TEXT("Generated-mode selector option coverage requires a selector id from the active selector roster."); + Surface.CoverageLine = TEXT("Case coverage: n/a"); + Surface.SelectionCoverageLine = TEXT("Current selection: n/a"); + Surface.DetailLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Option detail: n/a") + : FString::Printf(TEXT("Option detail: %s"), *OptionMenuSurface.StatusLine); + return Surface; + } + + for (const FHyperTwistTrainingCoachGeneratedModeSelectorOptionEntrySurface& OptionEntry : + OptionMenuSurface.OptionEntries) + { + FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilityEntrySurface Entry; + Entry.OptionId = OptionEntry.OptionId; + Entry.Label = OptionEntry.Label; + Entry.OptionValue = OptionEntry.OptionValue; + Entry.bMatchesCurrentSelection = OptionEntry.bMatchesCurrentSelection; + + FHyperTwistTrainingCase MatchingCase; + Entry.bHasCoveredCase = UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSelectorOptionCaseFromDeck( + ActiveDeck, + SelectorId, + OptionEntry.OptionId, + MatchingCase + ); + if (Entry.bHasCoveredCase) + { + Entry.MatchedCaseId = MatchingCase.CaseId; + Entry.MatchedCaseLabel = MatchingCase.PromptLabel.IsEmpty() + ? MatchingCase.CaseId + : MatchingCase.PromptLabel; + Entry.ActionabilityLine = Entry.MatchedCaseLabel.IsEmpty() + ? TEXT("Case coverage: owned case available") + : FString::Printf(TEXT("Case coverage: %s"), *Entry.MatchedCaseLabel); + ++Surface.CoveredOptionCount; + } + else + { + Entry.ActionabilityLine = TEXT("Case coverage: selection-only choice"); + ++Surface.SelectionOnlyOptionCount; + } + + if (Entry.bMatchesCurrentSelection) + { + Surface.bCurrentSelectionHasCaseCoverage = Entry.bHasCoveredCase; + Surface.bCurrentSelectionIsSelectionOnly = !Entry.bHasCoveredCase; + } + + Surface.OptionEntries.Add(MoveTemp(Entry)); + } + + Surface.bHasCaseCoverage = Surface.CoveredOptionCount > 0; + Surface.CoverageLine = Surface.OptionEntries.Num() > 0 + ? FString::Printf( + TEXT("Case coverage: %d covered | %d selection-only"), + Surface.CoveredOptionCount, + Surface.SelectionOnlyOptionCount + ) + : TEXT("Case coverage: no imported options"); + + if (OptionMenuSurface.bSelectionMatchesAvailableOption && Surface.bCurrentSelectionHasCaseCoverage) + { + Surface.SelectionCoverageLine = FString::Printf( + TEXT("Current selection maps to owned case: %s"), + OptionMenuSurface.MatchedOptionLabel.IsEmpty() + ? (OptionMenuSurface.MatchedOptionId.IsEmpty() + ? TEXT("n/a") + : *OptionMenuSurface.MatchedOptionId) + : *OptionMenuSurface.MatchedOptionLabel + ); + } + else if (OptionMenuSurface.bSelectionMatchesAvailableOption && Surface.bCurrentSelectionIsSelectionOnly) + { + Surface.SelectionCoverageLine = + TEXT("Current selection remains available, but it is currently selection-only."); + } + else if (OptionMenuSurface.bHasUnresolvedSelectionDetail) + { + Surface.SelectionCoverageLine = OptionMenuSurface.UnresolvedSelectionLine.IsEmpty() + ? TEXT("Current selection does not currently map to an imported option.") + : OptionMenuSurface.UnresolvedSelectionLine; + } + else if (OptionMenuSurface.bHasSelection) + { + Surface.SelectionCoverageLine = TEXT("Current selection has no additional case-coverage detail."); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.SelectionCoverageLine = TEXT("Current selection: unresolved"); + } + else + { + Surface.SelectionCoverageLine = TEXT("Current selection: n/a"); + } + + if (OptionMenuSurface.bHasSelector && Surface.CoveredOptionCount > 0 + && Surface.SelectionOnlyOptionCount == 0) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option coverage exposes %d covered option%s and no selection-only choices."), + Surface.CoveredOptionCount, + Surface.CoveredOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (OptionMenuSurface.bHasSelector && Surface.CoveredOptionCount > 0) + { + Surface.StatusLine = FString::Printf( + TEXT("Generated-mode selector option coverage exposes %d covered option%s and %d selection-only choice%s."), + Surface.CoveredOptionCount, + Surface.CoveredOptionCount == 1 ? TEXT("") : TEXT("s"), + Surface.SelectionOnlyOptionCount, + Surface.SelectionOnlyOptionCount == 1 ? TEXT("") : TEXT("s") + ); + } + else if (OptionMenuSurface.bHasSelector && OptionMenuSurface.bHasOptions) + { + Surface.StatusLine = + TEXT("Generated-mode selector option coverage is active, but none of the imported options currently map to owned cases."); + } + else if (OptionMenuSurface.bHasSelector) + { + Surface.StatusLine = + TEXT("Generated-mode selector option coverage is active, but this selector does not expose imported options."); + } + else + { + Surface.StatusLine = OptionMenuSurface.StatusLine.IsEmpty() + ? TEXT("Generated-mode selector option coverage is not currently available.") + : OptionMenuSurface.StatusLine; + } + + const FString SelectorSummary = Surface.SelectorLabel.IsEmpty() + ? (Surface.SelectorId.IsEmpty() ? FString(TEXT("n/a")) : Surface.SelectorId) + : Surface.SelectorLabel; + const FString BindingSummary = Surface.BindingLabel.IsEmpty() + ? FString(TEXT("n/a")) + : Surface.BindingLabel; + Surface.DetailLine = FString::Printf( + TEXT("Option detail: %s | Selector: %s | Binding: %s"), + OptionMenuSurface.StatusLine.IsEmpty() ? TEXT("n/a") : *OptionMenuSurface.StatusLine, + *SelectorSummary, + *BindingSummary + ); + + return Surface; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 2c6a857..e69710b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -719,6 +719,20 @@ UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionMenu ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface +UHyperTwistTrainingPanelWidget::GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionActionabilitySurface( + CachedRunState.ActiveDeck, + SelectorId, + OptionMenuSurface + ); +} + FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun( const int32 MaxCases, const FString& StartActionSourceLabel diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp index e668c14..966c6f4 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -700,6 +700,20 @@ AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionMen ); } +FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface +AHyperTwistTrainingSessionActor::GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface( + const FString& SelectorId +) const +{ + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface OptionMenuSurface = + GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(SelectorId); + return UHyperTwistTrainingCoachLibrary::DeriveCoachGeneratedModeSelectorOptionActionabilitySurface( + CachedRunState.ActiveDeck, + SelectorId, + OptionMenuSurface + ); +} + FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommendedTrainingRun( const int32 MaxCases ) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h index c762671..32d8d10 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -696,4 +696,12 @@ public: const FString& SelectorId, const FHyperTwistTrainingCoachGeneratedModeSelectorRosterSurface& SelectorRosterSurface ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface + DeriveCoachGeneratedModeSelectorOptionActionabilitySurface( + const FHyperTwistTrainingDeck& ActiveDeck, + const FString& SelectorId, + const FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface& OptionMenuSurface + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index ddaf56b..562a869 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -260,6 +260,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface + GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedRun( int32 MaxCases, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h index 8fba97e..5ea0b33 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -241,6 +241,10 @@ public: FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface GetDisplayedCoachGeneratedModeSelectorOptionMenuSurface(const FString& SelectorId) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface + GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedTrainingRun(int32 MaxCases); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 6676747..5be1ddf 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -8676,3 +8676,103 @@ struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionMenuSurface || bHasSelection; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilityEntrySurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString OptionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Label; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString OptionValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActionabilityLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MatchedCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MatchedCaseLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMatchesCurrentSelection = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCoveredCase = false; + + bool IsStructurallyValid() const + { + return !OptionId.IsEmpty() + || !Label.IsEmpty() + || !ActionabilityLine.IsEmpty() + || !MatchedCaseId.IsEmpty() + || bMatchesCurrentSelection + || bHasCoveredCase; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectorLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BindingLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CoverageLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SelectionCoverageLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray OptionEntries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CoveredOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SelectionOnlyOptionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasSelector = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCaseCoverage = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionHasCaseCoverage = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCurrentSelectionIsSelectionOnly = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !SelectorId.IsEmpty() + || OptionEntries.Num() > 0 + || bHasSelector + || bHasCaseCoverage; + } +}; diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 357d6a0..60ffaf6 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -68,8 +68,8 @@ Current correction call from the source-exposed audit and current execution refr - the imported generated-mode gap is closed; request/config/selection plumbing and the bounded clean-room executor are already landed in first-party code - the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete - the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle - - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin parameterized generated-mode selector option-menu / detail surface over the already-cached selector roster/runtime state, so gameplay/Blueprint callers can render normalized selector-option ids, labels, values, and unresolved-selection detail for a chosen selector without reading raw imported option payloads directly - - the current next bounded packet is to expose one thin generated-mode selector option actionability / case-coverage surface on those same two consumer surfaces so gameplay/Blueprint callers can render which imported options still map cleanly to owned cases and which remain selection-only choices without reading raw option-case payloads or runtime probes directly + - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds one thin generated-mode selector option actionability / case-coverage surface over the already-cached selector/runtime state, so gameplay/Blueprint callers can render which imported options still map cleanly to owned cases and which remain selection-only choices without reading raw option-case payloads or runtime probes directly + - the current next bounded packet is to expose one thin generated-mode selector option interaction / dispatch surface on those same two consumer surfaces so gameplay/Blueprint callers can render whether choosing an option should launch a direct owned case, apply a generated-mode selector choice, or stay unavailable without reaching into raw runtime action-path rules directly - the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it ## Current execution-reality references diff --git a/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_COVERAGE_SURFACE_PACKET_2026-05-08.md b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_COVERAGE_SURFACE_PACKET_2026-05-08.md new file mode 100644 index 0000000..1b7bf46 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_GENERATED_MODE_SELECTOR_OPTION_COVERAGE_SURFACE_PACKET_2026-05-08.md @@ -0,0 +1,104 @@ +# HyperTwist Phase 4 gameplay generated-mode selector option actionability / case-coverage surface packet + +Created on `2026-05-08` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` gameplay-facing consumer slice + +## Purpose + +This packet closes the next thin gameplay-facing consumer seam above the panel-widget and session-actor generated-mode selector option detail surface by exposing one parameterized option actionability / case-coverage surface for a chosen selector id. + +The open tasks were: + +- stop forcing gameplay / Blueprint callers to read raw option-case payloads or run their own option-to-case probes just to tell which imported options still map to owned cases +- expose which imported options remain selection-only choices even though they still exist in the selector option menu +- keep the surface derived from already-cached gameplay consumer selector/runtime state and the existing owned deck rather than widening into new backend behavior + +It is not: + +- a new selector-application behavior packet +- a generated-mode execution packet +- a selector option dispatch / interaction packet +- a broader gameplay UI composition pass + +## Scope + +Bounded lane: + +- add first-party generated-mode selector option actionability surfaces in shared training types +- add a coach-library helper that derives case coverage from the active deck, requested selector id, and the already-landed selector option-menu surface +- expose that parameterized actionability surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor` +- keep the derivation presentation-only and rooted in already-owned deck cases plus cached gameplay consumer selector state + +Out of scope: + +- changing selector-choice persistence +- changing generated-mode launch-config derivation rules +- changing direct option-launch or generated-choice apply behavior +- widening into per-option dispatch posture yet + +## Why this was the right next packet + +Before this slice: + +- gameplay-facing panel/session consumers already exposed truthful generated-mode launch / execution context +- those same consumers already exposed truthful selector / application context +- those same consumers already exposed truthful selector roster / option-availability summary +- those same consumers already exposed truthful chosen-selector option detail + +But one thin consumer seam was still open: + +- callers could render the option menu for a chosen selector +- but they still had to inspect raw option-case behavior to explain which options still mapped to owned cases and which were now selection-only choices + +That meant: + +- option detail was thin and reusable +- but case coverage still leaked low-level option matching work into callers + +So the next bounded move was: + +- keep the already-landed generated-mode context, roster, and option-detail surfaces unchanged +- and expose one narrow option actionability / case-coverage surface above the owned deck case set + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilityEntrySurface` + - added `FHyperTwistTrainingCoachGeneratedModeSelectorOptionActionabilitySurface` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` + - added `DeriveCoachGeneratedModeSelectorOptionActionabilitySurface(...)` + - case coverage is derived from the active owned deck, requested selector id, and the already-landed selector option-menu surface +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId)` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp` + - added `GetDisplayedCoachGeneratedModeSelectorOptionActionabilitySurface(const FString& SelectorId)` + +Surface behavior: + +- reports which imported options for the chosen selector still map to owned deck cases +- reports which imported options remain selection-only choices +- reports whether the current cached selection is case-covered, selection-only, or no longer present in the imported option list +- keeps the surface presentation-only and compatible with the already-landed generated-mode option-detail surface + +## Validation + +Validation target: + +- full `Development Editor|Win64` build of `C:\HyperTwist\UnrealHyperTwist\UnrealHyperTwist.sln` + +Result: + +- passed with `0` warnings and `0` errors + +## Follow-on + +The next bounded packet on this same gameplay-facing consumer lane should stay thin and expose one generated-mode selector option interaction / dispatch surface so callers can render whether choosing an option should launch a direct owned case, apply a generated-mode selector choice, or stay unavailable without reaching into raw runtime action-path rules directly.