From c9583b82da028f48903cc0c44ecca163fbb5daa4 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Tue, 19 May 2026 23:31:20 +0200 Subject: [PATCH] Implement Bound 4 publication entitlement bundle --- .../HyperTwistTrainingPublicationLibrary.cpp | 443 ++++++++++++++++++ .../HyperTwistTrainingRuntimeLibrary.cpp | 12 + .../HyperTwistTrainingSubsystem.cpp | 39 ++ .../HyperTwistTrainingPublicationLibrary.h | 26 + .../HyperTwistTrainingRuntimeLibrary.h | 3 + .../HyperTwistTrainingSubsystem.h | 6 + .../HyperTwistTrainingTypes.h | 259 ++++++++++ ...beDeskBound4PublicationEntitlementTest.cpp | 354 ++++++++++++++ 8 files changed, 1142 insertions(+) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistCubeDeskBound4PublicationEntitlementTest.cpp diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.cpp new file mode 100644 index 0000000..8f03283 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.cpp @@ -0,0 +1,443 @@ +#include "HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h" + +namespace HyperTwistTrainingPublicationLibraryInternal +{ + const FString PublicationFeatureId = TEXT("feature/training/competitive-publication"); + const FString PublicationGateId = TEXT("gate/training/competitive-publication"); + + bool ArrayContainsCaseInsensitive(const TArray& Values, const FString& Target) + { + for (const FString& Value : Values) + { + if (Value.Equals(Target, ESearchCase::IgnoreCase)) + { + return true; + } + } + + return false; + } + + bool ArrayContainsSubstringCaseInsensitive(const TArray& Values, const FString& Fragment) + { + for (const FString& Value : Values) + { + if (Value.Contains(Fragment, ESearchCase::IgnoreCase)) + { + return true; + } + } + + return false; + } + + void AppendUniqueTag(TArray& Tags, const FString& Tag) + { + if (Tag.IsEmpty() || Tags.Contains(Tag)) + { + return; + } + + Tags.Add(Tag); + } + + FString ResolveUserId( + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary + ) + { + if (!LearnerProfile.UserId.IsEmpty()) + { + return LearnerProfile.UserId; + } + if (!DeckHistorySummary.UserId.IsEmpty()) + { + return DeckHistorySummary.UserId; + } + return PersonalBestSummary.UserId; + } + + FString ResolveDeckId( + const FHyperTwistTrainingDeck& Deck, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary + ) + { + if (!Deck.DeckId.IsEmpty()) + { + return Deck.DeckId; + } + if (!DeckHistorySummary.DeckId.IsEmpty()) + { + return DeckHistorySummary.DeckId; + } + return PersonalBestSummary.DeckId; + } + + FString ResolveDeckPuzzleId(const FHyperTwistTrainingDeck& Deck) + { + for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases) + { + if (TrainingCase.IsStructurallyValid()) + { + return TrainingCase.PuzzleId; + } + } + + return FString(); + } + + FString ResolveLifecycleStageId(const FHyperTwistTrainingEarlyAccessRolloutContract& EarlyAccessContract) + { + static const TArray PreferredStageOrder = + { + TEXT("general-availability"), + TEXT("beta"), + TEXT("alpha"), + TEXT("concept"), + TEXT("draft"), + TEXT("archived") + }; + + for (const FString& StageId : PreferredStageOrder) + { + if (ArrayContainsCaseInsensitive(EarlyAccessContract.ActiveStages, StageId)) + { + return StageId; + } + } + + for (const FString& StageId : PreferredStageOrder) + { + if (ArrayContainsCaseInsensitive(EarlyAccessContract.LifecycleStages, StageId)) + { + return StageId; + } + } + + return EarlyAccessContract.ActiveStages.Num() > 0 + ? EarlyAccessContract.ActiveStages[0] + : (EarlyAccessContract.LifecycleStages.Num() > 0 ? EarlyAccessContract.LifecycleStages[0] : FString()); + } + + bool IsVisibleLifecycleStage(const FString& LifecycleStageId) + { + return LifecycleStageId.Equals(TEXT("alpha"), ESearchCase::IgnoreCase) + || LifecycleStageId.Equals(TEXT("beta"), ESearchCase::IgnoreCase) + || LifecycleStageId.Equals(TEXT("general-availability"), ESearchCase::IgnoreCase); + } + + FHyperTwistTrainingEntitlementGateState BuildEntitlementGateState( + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + const FHyperTwistTrainingFeatureGovernanceContract& FeatureGovernanceContract, + const FHyperTwistTrainingEarlyAccessRolloutContract& EarlyAccessContract + ) + { + FHyperTwistTrainingEntitlementGateState Gate; + Gate.GateId = PublicationGateId; + Gate.FeatureId = PublicationFeatureId; + Gate.Title = TEXT("Competitive publication and leaderboard access"); + Gate.FeatureGovernanceContractId = FeatureGovernanceContract.ContractId; + Gate.EarlyAccessContractId = EarlyAccessContract.ContractId; + Gate.LifecycleStageId = ResolveLifecycleStageId(EarlyAccessContract); + Gate.EnrollmentModeLabel = + ArrayContainsSubstringCaseInsensitive(EarlyAccessContract.EnrollmentRules, TEXT("opt-in")) + ? TEXT("opt-in") + : TEXT("automatic"); + Gate.ApprovedWorkflowTags = FeatureGovernanceContract.ApprovedWorkflowTags; + Gate.bRequiresOptIn = Gate.EnrollmentModeLabel.Equals(TEXT("opt-in"), ESearchCase::CaseSensitive); + + const bool bHasGovernanceSupport = + ArrayContainsCaseInsensitive(FeatureGovernanceContract.ApprovedWorkflowTags, TEXT("feature-governance")); + const bool bHasVisibleLifecycleStage = IsVisibleLifecycleStage(Gate.LifecycleStageId); + const bool bHasActivity = LearnerProfile.TotalSessionCount > 0 || DeckHistorySummary.SessionCount > 0; + const bool bHasEarnedPublication = + LearnerProfile.CompletedSessionCount > 0 + || DeckHistorySummary.CompletedSessionCount > 0 + || PersonalBestSummary.bHasTimedPersonalBest; + + Gate.bVisible = bHasGovernanceSupport && bHasVisibleLifecycleStage && bHasActivity; + Gate.bEntitled = Gate.bVisible && bHasEarnedPublication; + Gate.VisibilityLabel = Gate.bEntitled + ? TEXT("published") + : (Gate.bVisible ? TEXT("preview") : TEXT("hidden")); + return Gate; + } + + EHyperTwistTrainingPublicationVisibility ResolvePublicationVisibility( + const FHyperTwistTrainingEntitlementGateState& GateState + ) + { + if (GateState.bEntitled) + { + return EHyperTwistTrainingPublicationVisibility::Published; + } + if (GateState.bVisible) + { + return EHyperTwistTrainingPublicationVisibility::Preview; + } + return EHyperTwistTrainingPublicationVisibility::Hidden; + } + + void ResolveHighlightValues( + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + FString& OutPrimaryValue, + FString& OutSecondaryValue + ) + { + OutPrimaryValue.Reset(); + OutSecondaryValue.Reset(); + if (PersonalBestSummary.bHasTimedPersonalBest && PersonalBestSummary.BestSingleTotalTimeMs > 0) + { + const FHyperTwistTrainingLeaderboardFormattedValue FormattedValue = + UHyperTwistTrainingKnowledgeLibrary::FormatHypercubingLeaderboardDurationMilliseconds( + PersonalBestSummary.BestSingleTotalTimeMs + ); + OutPrimaryValue = FormattedValue.PrimaryLabel; + OutSecondaryValue = FormattedValue.SecondaryLabel; + } + } + + FHyperTwistTrainingProfilePublication BuildProfilePublication( + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + const FHyperTwistTrainingDeck& Deck, + const FHyperTwistTrainingEntitlementGateState& GateState + ) + { + FHyperTwistTrainingProfilePublication Publication; + Publication.UserId = ResolveUserId(LearnerProfile, DeckHistorySummary, PersonalBestSummary); + Publication.DeckId = ResolveDeckId(Deck, DeckHistorySummary, PersonalBestSummary); + Publication.DeckTitle = Deck.Title; + Publication.PublicationId = FString::Printf(TEXT("publication/%s/%s"), *Publication.UserId, *Publication.DeckId); + Publication.DisplayName = Publication.UserId; + Publication.Visibility = ResolvePublicationVisibility(GateState); + Publication.Headline = Publication.Visibility == EHyperTwistTrainingPublicationVisibility::Published + ? FString::Printf( + TEXT("%d completed sessions, %d day streak"), + LearnerProfile.CompletedSessionCount, + LearnerProfile.ActiveTrainingDayStreak + ) + : (Publication.Visibility == EHyperTwistTrainingPublicationVisibility::Preview + ? TEXT("Preview publication is ready for local review") + : TEXT("Complete training to unlock publication")); + Publication.SummaryLine = FString::Printf( + TEXT("%d deck sessions, %d attempts, %.0f%% success rate"), + DeckHistorySummary.SessionCount, + DeckHistorySummary.TotalAttemptCount, + LearnerProfile.SuccessRate + ); + Publication.HighlightLabel = PersonalBestSummary.bHasTimedPersonalBest ? TEXT("Best single") : TEXT("Activity"); + ResolveHighlightValues( + PersonalBestSummary, + Publication.HighlightPrimaryValue, + Publication.HighlightSecondaryValue + ); + if (Publication.HighlightPrimaryValue.IsEmpty()) + { + Publication.HighlightPrimaryValue = FString::Printf( + TEXT("%d sessions"), + LearnerProfile.TotalSessionCount + ); + } + Publication.LastPublishedAtUtc = !PersonalBestSummary.LastPersonalBestAtUtc.IsEmpty() + ? PersonalBestSummary.LastPersonalBestAtUtc + : LearnerProfile.LastTrainingAtUtc; + for (const FString& Tag : Deck.Tags) + { + AppendUniqueTag(Publication.Tags, Tag); + } + AppendUniqueTag(Publication.Tags, GateState.LifecycleStageId); + AppendUniqueTag(Publication.Tags, TEXT("competitive-publication")); + return Publication; + } + + bool TryBuildEntryMetric( + const FHyperTwistTrainingLeaderboardEventReference& EventReference, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + FString& OutPrimaryValue, + FString& OutSecondaryValue + ) + { + OutPrimaryValue.Reset(); + OutSecondaryValue.Reset(); + + if (EventReference.FormatId.Equals(TEXT("single"), ESearchCase::IgnoreCase) + && PersonalBestSummary.bHasTimedPersonalBest + && PersonalBestSummary.BestSingleTotalTimeMs > 0) + { + const FHyperTwistTrainingLeaderboardFormattedValue FormattedValue = + UHyperTwistTrainingKnowledgeLibrary::FormatHypercubingLeaderboardDurationMilliseconds( + PersonalBestSummary.BestSingleTotalTimeMs + ); + OutPrimaryValue = FormattedValue.PrimaryLabel; + OutSecondaryValue = FormattedValue.SecondaryLabel; + return !OutPrimaryValue.IsEmpty(); + } + + if ((EventReference.FormatId.Contains(TEXT("ao"), ESearchCase::IgnoreCase) + || EventReference.FormatId.Contains(TEXT("mean"), ESearchCase::IgnoreCase)) + && PersonalBestSummary.BestSessionAverageTotalTimeMs > 0) + { + const FHyperTwistTrainingLeaderboardFormattedValue FormattedValue = + UHyperTwistTrainingKnowledgeLibrary::FormatHypercubingLeaderboardDurationMilliseconds( + PersonalBestSummary.BestSessionAverageTotalTimeMs + ); + OutPrimaryValue = FormattedValue.PrimaryLabel; + OutSecondaryValue = FormattedValue.SecondaryLabel; + return !OutPrimaryValue.IsEmpty(); + } + + return false; + } + + FHyperTwistTrainingLeaderboardBundle BuildLeaderboardBundle( + const FHyperTwistTrainingProfilePublication& Publication, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + const FHyperTwistTrainingDeck& Deck, + const FHyperTwistTrainingKnowledgeReferenceBundle& KnowledgeBundle, + const FHyperTwistTrainingEntitlementGateState& GateState + ) + { + FHyperTwistTrainingLeaderboardBundle Leaderboard; + Leaderboard.BundleId = FString::Printf(TEXT("leaderboard/%s/%s"), *Publication.UserId, *Publication.DeckId); + Leaderboard.DeckId = Publication.DeckId; + Leaderboard.DeckTitle = Publication.DeckTitle; + Leaderboard.ColumnOrder = KnowledgeBundle.LeaderboardDisplayPolicy.ColumnOrder; + Leaderboard.TimeFormattingRule = KnowledgeBundle.LeaderboardDisplayPolicy.TimeFormattingRule; + Leaderboard.RelativeDateRule = KnowledgeBundle.LeaderboardDisplayPolicy.RelativeDateRule; + Leaderboard.SolverProfileRule = KnowledgeBundle.LeaderboardDisplayPolicy.SolverProfileRule; + Leaderboard.ProgramBadgeRule = KnowledgeBundle.LeaderboardDisplayPolicy.ProgramBadgeRule; + Leaderboard.bEntitlementVisible = GateState.bVisible; + + const FString DeckPuzzleId = ResolveDeckPuzzleId(Deck); + const FString RecordedAtUtc = !PersonalBestSummary.LastPersonalBestAtUtc.IsEmpty() + ? PersonalBestSummary.LastPersonalBestAtUtc + : Publication.LastPublishedAtUtc; + + for (const FHyperTwistTrainingLeaderboardEventReference& EventReference : KnowledgeBundle.LeaderboardEvents) + { + if (!EventReference.PuzzleId.Equals(DeckPuzzleId, ESearchCase::IgnoreCase)) + { + continue; + } + + ++Leaderboard.MatchedEventCount; + if (Leaderboard.PrimaryEventId.IsEmpty()) + { + Leaderboard.PrimaryEventId = EventReference.EventId; + Leaderboard.PrimaryEventTitle = EventReference.Title; + } + + FString MetricPrimaryValue; + FString MetricSecondaryValue; + if (!TryBuildEntryMetric( + EventReference, + PersonalBestSummary, + MetricPrimaryValue, + MetricSecondaryValue)) + { + continue; + } + + FHyperTwistTrainingLeaderboardEntry Entry; + Entry.EntryId = FString::Printf(TEXT("leaderboard-entry/%s/%s"), *EventReference.EventId, *Publication.UserId); + Entry.EventId = EventReference.EventId; + Entry.EventTitle = EventReference.Title; + Entry.MetricLabel = EventReference.MetricLabel; + Entry.SolverDisplayName = Publication.DisplayName; + Entry.ProgramLabel = EventReference.PreferredProgramIds.Num() > 0 + ? EventReference.PreferredProgramIds[0] + : TEXT("Local"); + Entry.RecordedAtUtc = RecordedAtUtc; + Entry.RelativeDateLabel = !RecordedAtUtc.IsEmpty() + ? UHyperTwistTrainingKnowledgeLibrary::FormatHypercubingLeaderboardRelativeDateLabel(RecordedAtUtc) + : TEXT("n/a"); + Entry.RankLabel = GateState.bEntitled ? TEXT("#1 local") : TEXT("preview"); + Entry.MetricPrimaryValue = MetricPrimaryValue; + Entry.MetricSecondaryValue = MetricSecondaryValue; + Entry.DeckId = Publication.DeckId; + Entry.bEligible = GateState.bVisible; + Entry.bPublished = GateState.bEntitled; + Leaderboard.Entries.Add(Entry); + } + + for (const FHyperTwistTrainingLeaderboardEntry& Entry : Leaderboard.Entries) + { + if (Entry.bPublished) + { + Leaderboard.bHasPublishedEntries = true; + break; + } + } + + return Leaderboard; + } +} + +FHyperTwistTrainingPublicationBundle UHyperTwistTrainingPublicationLibrary::BuildPublicationBundle( + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + const FHyperTwistTrainingDeck& Deck, + const FHyperTwistTrainingKnowledgeReferenceBundle& KnowledgeBundle, + const FHyperTwistTrainingFeatureGovernanceContract& FeatureGovernanceContract, + const FHyperTwistTrainingEarlyAccessRolloutContract& EarlyAccessContract +) +{ + FHyperTwistTrainingPublicationBundle Bundle; + Bundle.UserId = HyperTwistTrainingPublicationLibraryInternal::ResolveUserId( + LearnerProfile, + DeckHistorySummary, + PersonalBestSummary + ); + Bundle.DeckId = HyperTwistTrainingPublicationLibraryInternal::ResolveDeckId( + Deck, + DeckHistorySummary, + PersonalBestSummary + ); + Bundle.DeckTitle = Deck.Title; + Bundle.LearnerProfile = LearnerProfile; + Bundle.DeckHistorySummary = DeckHistorySummary; + Bundle.PersonalBestSummary = PersonalBestSummary; + + if (Bundle.UserId.IsEmpty() + || Bundle.DeckId.IsEmpty() + || !Deck.IsStructurallyValid() + || !KnowledgeBundle.LeaderboardDisplayPolicy.IsStructurallyValid() + || !FeatureGovernanceContract.IsStructurallyValid() + || !EarlyAccessContract.IsStructurallyValid()) + { + return Bundle; + } + + Bundle.EntitlementGate = HyperTwistTrainingPublicationLibraryInternal::BuildEntitlementGateState( + LearnerProfile, + DeckHistorySummary, + PersonalBestSummary, + FeatureGovernanceContract, + EarlyAccessContract + ); + Bundle.ProfilePublication = HyperTwistTrainingPublicationLibraryInternal::BuildProfilePublication( + LearnerProfile, + DeckHistorySummary, + PersonalBestSummary, + Deck, + Bundle.EntitlementGate + ); + Bundle.Leaderboard = HyperTwistTrainingPublicationLibraryInternal::BuildLeaderboardBundle( + Bundle.ProfilePublication, + PersonalBestSummary, + Deck, + KnowledgeBundle, + Bundle.EntitlementGate + ); + Bundle.bHasMatchedLeaderboardEvents = Bundle.Leaderboard.MatchedEventCount > 0; + Bundle.ProfilePublication.bLeaderboardVisible = + Bundle.EntitlementGate.bVisible && Bundle.Leaderboard.Entries.Num() > 0; + return Bundle; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 1d8c058..9432b06 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -1422,6 +1422,18 @@ FHyperTwistTrainingLearnerProfile UHyperTwistTrainingRuntimeLibrary::GetActiveTr return FHyperTwistTrainingLearnerProfile(); } +FHyperTwistTrainingPublicationBundle UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingPublicationBundle( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActivePublicationBundle(); + } + + return FHyperTwistTrainingPublicationBundle(); +} + FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachMemorySnapshot(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index fe1aaa2..76bb7c3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -9,6 +9,7 @@ #include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" #include "JsonObjectConverter.h" @@ -3063,6 +3064,11 @@ FHyperTwistTrainingLearnerProfile UHyperTwistTrainingSubsystem::GetActiveLearner return ActiveLearnerProfile; } +FHyperTwistTrainingPublicationBundle UHyperTwistTrainingSubsystem::GetActivePublicationBundle() const +{ + return ActivePublicationBundle; +} + FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingSubsystem::GetActiveCoachMemorySnapshot() const { return ActiveCoachMemorySnapshot; @@ -6940,6 +6946,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCurrentCaseScopedStats = FHyperTwistTrainingCaseScopedStats(); ActiveLearnerDeckStateSummary = FHyperTwistTrainingLearnerDeckStateSummary(); ActiveLearnerProfile = FHyperTwistTrainingLearnerProfile(); + ActivePublicationBundle = FHyperTwistTrainingPublicationBundle(); ActiveCoachMemorySnapshot = FHyperTwistTrainingCoachMemorySnapshot(); ActiveReplayReviewAnalytics = FHyperTwistReplayReviewAnalytics(); ActiveRecognitionReplaySummary = FHyperTwistRecognitionReplaySummary(); @@ -7153,6 +7160,38 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() 5 ) : TArray(); + { + const FHyperTwistTrainingKnowledgeReferenceBundle KnowledgeBundle = + UHyperTwistTrainingKnowledgeLibrary::MakeHypercubingKnowledgeReferenceBundle(); + const FHyperTwistTrainingControlPlaneReferenceBundle ControlPlaneBundle = + UHyperTwistTrainingControlPlaneLibrary::BuildBundledControlPlaneReferenceBundle(); + FHyperTwistTrainingFeatureGovernanceContract FeatureGovernanceContract; + FHyperTwistTrainingEarlyAccessRolloutContract EarlyAccessContract; + const bool bHasFeatureGovernanceContract = + UHyperTwistTrainingControlPlaneLibrary::TryGetFeatureGovernanceContractById( + ControlPlaneBundle, + ControlPlaneBundle.PrimaryFeatureGovernanceContractId, + FeatureGovernanceContract + ); + const bool bHasEarlyAccessContract = + UHyperTwistTrainingControlPlaneLibrary::TryGetEarlyAccessRolloutContractById( + ControlPlaneBundle, + ControlPlaneBundle.PrimaryEarlyAccessRolloutContractId, + EarlyAccessContract + ); + ActivePublicationBundle = + RepositoryFocusDeck.IsStructurallyValid() && bHasFeatureGovernanceContract && bHasEarlyAccessContract + ? UHyperTwistTrainingPublicationLibrary::BuildPublicationBundle( + ActiveLearnerProfile, + ActiveDeckHistorySummary, + ActivePersonalBestSummary, + RepositoryFocusDeck, + KnowledgeBundle, + FeatureGovernanceContract, + EarlyAccessContract + ) + : FHyperTwistTrainingPublicationBundle(); + } ActiveReviewFlowStatus = ActiveReviewPlanState.IsStructurallyValid() ? UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(ActiveReviewPlanState) : FHyperTwistTrainingReviewFlowStatus(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h new file mode 100644 index 0000000..183ac86 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h @@ -0,0 +1,26 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingControlPlaneLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingKnowledgeLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingTypes.h" +#include "HyperTwistTrainingPublicationLibrary.generated.h" + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistTrainingPublicationLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Publication") + static FHyperTwistTrainingPublicationBundle BuildPublicationBundle( + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingDeckHistorySummary& DeckHistorySummary, + const FHyperTwistTrainingPersonalBestSummary& PersonalBestSummary, + const FHyperTwistTrainingDeck& Deck, + const FHyperTwistTrainingKnowledgeReferenceBundle& KnowledgeBundle, + const FHyperTwistTrainingFeatureGovernanceContract& FeatureGovernanceContract, + const FHyperTwistTrainingEarlyAccessRolloutContract& EarlyAccessContract + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 038df69..2393f6c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -565,6 +565,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingLearnerProfile GetActiveTrainingLearnerProfile(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Publication", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingPublicationBundle GetActiveTrainingPublicationBundle(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingCoachMemorySnapshot GetActiveTrainingCoachMemorySnapshot(UObject* WorldContextObject); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index c2483db..7245ff6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -158,6 +158,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingLearnerProfile GetActiveLearnerProfile() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Publication") + FHyperTwistTrainingPublicationBundle GetActivePublicationBundle() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingCoachMemorySnapshot GetActiveCoachMemorySnapshot() const; @@ -750,6 +753,9 @@ private: UPROPERTY() FHyperTwistTrainingLearnerProfile ActiveLearnerProfile; + UPROPERTY() + FHyperTwistTrainingPublicationBundle ActivePublicationBundle; + UPROPERTY() FHyperTwistTrainingCoachMemorySnapshot ActiveCoachMemorySnapshot; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 405f8a7..0dc7d9d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -3256,6 +3256,265 @@ struct FHyperTwistTrainingLearnerProfile int32 OpenReviewPlanCount = 0; }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingPublicationVisibility : uint8 +{ + Hidden UMETA(DisplayName = "Hidden"), + Preview UMETA(DisplayName = "Preview"), + Published UMETA(DisplayName = "Published") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingEntitlementGateState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString GateId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FeatureId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Title; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FeatureGovernanceContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EarlyAccessContractId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LifecycleStageId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString VisibilityLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EnrollmentModeLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray ApprovedWorkflowTags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresOptIn = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bEntitled = false; + + bool IsStructurallyValid() const + { + return !GateId.IsEmpty() && !FeatureId.IsEmpty() && !Title.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingProfilePublication +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PublicationId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DisplayName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HighlightLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HighlightPrimaryValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HighlightSecondaryValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastPublishedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Tags; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingPublicationVisibility Visibility = EHyperTwistTrainingPublicationVisibility::Hidden; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLeaderboardVisible = false; + + bool IsStructurallyValid() const + { + return !PublicationId.IsEmpty() && !UserId.IsEmpty() && !DeckId.IsEmpty() && !DisplayName.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingLeaderboardEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EventId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EventTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MetricLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SolverDisplayName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ProgramLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecordedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RelativeDateLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RankLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MetricPrimaryValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MetricSecondaryValue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bEligible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPublished = false; + + bool IsStructurallyValid() const + { + return !EntryId.IsEmpty() && !EventId.IsEmpty() && !MetricPrimaryValue.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingLeaderboardBundle +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BundleId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryEventId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryEventTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray ColumnOrder; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TimeFormattingRule; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RelativeDateRule; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SolverProfileRule; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ProgramBadgeRule; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Entries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 MatchedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bEntitlementVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasPublishedEntries = false; + + bool IsStructurallyValid() const + { + return !BundleId.IsEmpty() && !DeckId.IsEmpty() && ColumnOrder.Num() > 0; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingPublicationBundle +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingLearnerProfile LearnerProfile; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingDeckHistorySummary DeckHistorySummary; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingPersonalBestSummary PersonalBestSummary; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingEntitlementGateState EntitlementGate; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingProfilePublication ProfilePublication; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistTrainingLeaderboardBundle Leaderboard; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasMatchedLeaderboardEvents = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() + && !DeckId.IsEmpty() + && EntitlementGate.IsStructurallyValid() + && ProfilePublication.IsStructurallyValid() + && Leaderboard.IsStructurallyValid(); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachMemorySnapshot { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistCubeDeskBound4PublicationEntitlementTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistCubeDeskBound4PublicationEntitlementTest.cpp new file mode 100644 index 0000000..e67b13b --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistCubeDeskBound4PublicationEntitlementTest.cpp @@ -0,0 +1,354 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "Misc/AutomationTest.h" + +#include "HyperTwistTraining/HyperTwistTrainingControlPlaneLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingKnowledgeLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h" + +#if WITH_AUTOMATION_TESTS + +namespace HyperTwistCubeDeskBound4PublicationEntitlementTestInternal +{ + FHyperTwistTrainingDeck MakeHypercubeDeck(const FString& DeckId) + { + FHyperTwistTrainingDeck Deck; + Deck.DeckId = DeckId; + Deck.Title = TEXT("3x3x3x3 Fundamentals"); + Deck.DeliveryModes = {EHyperTwistTrainingDeliveryMode::Timer}; + Deck.Tags = {TEXT("4d"), TEXT("hypercube"), TEXT("competitive")}; + + FHyperTwistTrainingCase TrainingCase; + TrainingCase.CaseId = TEXT("bound4-case-1"); + TrainingCase.PuzzleId = TEXT("3x3x3x3"); + TrainingCase.PromptLabel = TEXT("Bound4 Hypercube Case"); + TrainingCase.AllowedDeliveryModes = {EHyperTwistTrainingDeliveryMode::Timer}; + Deck.Cases.Add(TrainingCase); + return Deck; + } + + FHyperTwistTrainingLearnerProfile MakeLearnerProfile( + const FString& UserId, + const int32 TotalSessionCount, + const int32 CompletedSessionCount + ) + { + FHyperTwistTrainingLearnerProfile Profile; + Profile.UserId = UserId; + Profile.ReferenceUtc = TEXT("2026-05-19T12:00:00Z"); + Profile.TotalSessionCount = TotalSessionCount; + Profile.CompletedSessionCount = CompletedSessionCount; + Profile.TotalAttemptCount = 24; + Profile.TotalSuccessCount = 18; + Profile.TotalFailureCount = 6; + Profile.SuccessRate = 75.0f; + Profile.ActiveTrainingDayStreak = 5; + Profile.LongestObservedTrainingDayStreak = 8; + Profile.PrimaryDeliveryMode = EHyperTwistTrainingDeliveryMode::Timer; + Profile.FavoriteDeckId = TEXT("deck/hypercube-3333"); + Profile.FavoriteDeckSessionShare = 0.85f; + Profile.LastTrainingAtUtc = TEXT("2026-05-19T11:58:00Z"); + return Profile; + } + + FHyperTwistTrainingDeckHistorySummary MakeDeckHistorySummary(const FString& UserId, const FString& DeckId) + { + FHyperTwistTrainingDeckHistorySummary Summary; + Summary.UserId = UserId; + Summary.DeckId = DeckId; + Summary.SessionCount = 4; + Summary.CompletedSessionCount = 3; + Summary.TotalAttemptCount = 24; + Summary.SuccessCount = 18; + Summary.FailureCount = 4; + Summary.TimeoutCount = 2; + Summary.BestTotalTimeMs = 1890; + Summary.BestRawSolveTimeMs = 1710; + Summary.AverageTotalTimeMs = 2330; + Summary.AverageRawSolveTimeMs = 2140; + Summary.LastSessionId = TEXT("bound4-session"); + Summary.LastCompletedAtUtc = TEXT("2026-05-19T11:58:00Z"); + return Summary; + } + + FHyperTwistTrainingPersonalBestSummary MakePersonalBestSummary(const FString& UserId, const FString& DeckId) + { + FHyperTwistTrainingPersonalBestSummary Summary; + Summary.UserId = UserId; + Summary.DeckId = DeckId; + Summary.ReferenceUtc = TEXT("2026-05-19T11:58:00Z"); + Summary.BestSingleSessionId = TEXT("bound4-session"); + Summary.BestSingleTotalTimeMs = 1890; + Summary.BestSingleRawSolveTimeMs = 1710; + Summary.BestAverageSessionId = TEXT("bound4-session"); + Summary.BestSessionAverageTotalTimeMs = 2050; + Summary.BestSessionAverageRawSolveTimeMs = 1930; + Summary.LastPersonalBestAtUtc = TEXT("2026-05-19T11:58:00Z"); + Summary.bHasTimedPersonalBest = true; + return Summary; + } + + bool ResolveControlPlaneContracts( + FHyperTwistTrainingFeatureGovernanceContract& OutFeatureGovernanceContract, + FHyperTwistTrainingEarlyAccessRolloutContract& OutEarlyAccessContract + ) + { + const FHyperTwistTrainingControlPlaneReferenceBundle ControlPlaneBundle = + UHyperTwistTrainingControlPlaneLibrary::BuildBundledControlPlaneReferenceBundle(); + return UHyperTwistTrainingControlPlaneLibrary::TryGetFeatureGovernanceContractById( + ControlPlaneBundle, + ControlPlaneBundle.PrimaryFeatureGovernanceContractId, + OutFeatureGovernanceContract + ) + && UHyperTwistTrainingControlPlaneLibrary::TryGetEarlyAccessRolloutContractById( + ControlPlaneBundle, + ControlPlaneBundle.PrimaryEarlyAccessRolloutContractId, + OutEarlyAccessContract + ); + } + + const FHyperTwistTrainingLeaderboardEntry* FindEntryByEventId( + const FHyperTwistTrainingLeaderboardBundle& LeaderboardBundle, + const FString& EventId + ) + { + for (const FHyperTwistTrainingLeaderboardEntry& Entry : LeaderboardBundle.Entries) + { + if (Entry.EventId == EventId) + { + return &Entry; + } + } + + return nullptr; + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistCubeDeskBound4PublicationBundleTest, + "HyperTwist.CleanRoom.CubeDesk.Bound4.PublicationBundle", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistCubeDeskBound4PublicationBundleTest::RunTest(const FString& Parameters) +{ + FHyperTwistTrainingFeatureGovernanceContract FeatureGovernanceContract; + FHyperTwistTrainingEarlyAccessRolloutContract EarlyAccessContract; + TestTrue( + TEXT("Control-plane publication contracts must resolve from the bundled PostHog packet."), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::ResolveControlPlaneContracts( + FeatureGovernanceContract, + EarlyAccessContract + ) + ); + + const FHyperTwistTrainingDeck Deck = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeHypercubeDeck( + TEXT("deck/hypercube-3333") + ); + const FHyperTwistTrainingKnowledgeReferenceBundle KnowledgeBundle = + UHyperTwistTrainingKnowledgeLibrary::MakeHypercubingKnowledgeReferenceBundle(); + const FHyperTwistTrainingPublicationBundle Bundle = + UHyperTwistTrainingPublicationLibrary::BuildPublicationBundle( + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeLearnerProfile( + TEXT("bound4-user"), + 4, + 3 + ), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeDeckHistorySummary( + TEXT("bound4-user"), + Deck.DeckId + ), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakePersonalBestSummary( + TEXT("bound4-user"), + Deck.DeckId + ), + Deck, + KnowledgeBundle, + FeatureGovernanceContract, + EarlyAccessContract + ); + + TestTrue(TEXT("The publication bundle must resolve as structurally valid."), Bundle.IsStructurallyValid()); + TestTrue(TEXT("The entitlement gate must be visible for active learners."), Bundle.EntitlementGate.bVisible); + TestTrue(TEXT("The entitlement gate must resolve to entitled when local publication exists."), Bundle.EntitlementGate.bEntitled); + TestEqual( + TEXT("The publication bundle must promote to the published visibility state."), + Bundle.ProfilePublication.Visibility, + EHyperTwistTrainingPublicationVisibility::Published + ); + TestEqual( + TEXT("The publication bundle must retain the active deck title."), + Bundle.ProfilePublication.DeckTitle, + Deck.Title + ); + TestEqual( + TEXT("The publication bundle must retain the active user as its display name."), + Bundle.ProfilePublication.DisplayName, + TEXT("bound4-user") + ); + TestTrue( + TEXT("The publication bundle must expose a formatted highlight value."), + !Bundle.ProfilePublication.HighlightPrimaryValue.IsEmpty() + ); + TestTrue( + TEXT("The publication bundle must report matched leaderboard events for the hypercube deck."), + Bundle.bHasMatchedLeaderboardEvents + ); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistCubeDeskBound4EntitlementGateTest, + "HyperTwist.CleanRoom.CubeDesk.Bound4.EntitlementGate", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistCubeDeskBound4EntitlementGateTest::RunTest(const FString& Parameters) +{ + FHyperTwistTrainingFeatureGovernanceContract FeatureGovernanceContract; + FHyperTwistTrainingEarlyAccessRolloutContract EarlyAccessContract; + TestTrue( + TEXT("Control-plane publication contracts must resolve for entitlement gating."), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::ResolveControlPlaneContracts( + FeatureGovernanceContract, + EarlyAccessContract + ) + ); + + const FHyperTwistTrainingDeck Deck = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeHypercubeDeck( + TEXT("deck/hypercube-preview") + ); + FHyperTwistTrainingDeckHistorySummary DeckHistorySummary = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeDeckHistorySummary( + TEXT("bound4-preview-user"), + Deck.DeckId + ); + DeckHistorySummary.CompletedSessionCount = 0; + FHyperTwistTrainingPersonalBestSummary PersonalBestSummary = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakePersonalBestSummary( + TEXT("bound4-preview-user"), + Deck.DeckId + ); + PersonalBestSummary.bHasTimedPersonalBest = false; + PersonalBestSummary.BestSingleTotalTimeMs = 0; + PersonalBestSummary.BestSessionAverageTotalTimeMs = 0; + + const FHyperTwistTrainingPublicationBundle Bundle = + UHyperTwistTrainingPublicationLibrary::BuildPublicationBundle( + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeLearnerProfile( + TEXT("bound4-preview-user"), + 2, + 0 + ), + DeckHistorySummary, + PersonalBestSummary, + Deck, + UHyperTwistTrainingKnowledgeLibrary::MakeHypercubingKnowledgeReferenceBundle(), + FeatureGovernanceContract, + EarlyAccessContract + ); + + TestTrue(TEXT("The preview publication bundle must still be structurally valid."), Bundle.IsStructurallyValid()); + TestTrue(TEXT("Preview publication must still be visible when the lifecycle stage is active."), Bundle.EntitlementGate.bVisible); + TestFalse(TEXT("Preview publication must not be entitled without earned local publication data."), Bundle.EntitlementGate.bEntitled); + TestEqual( + TEXT("Preview publication must remain in the preview visibility state."), + Bundle.ProfilePublication.Visibility, + EHyperTwistTrainingPublicationVisibility::Preview + ); + TestEqual( + TEXT("The entitlement visibility label must describe preview posture explicitly."), + Bundle.EntitlementGate.VisibilityLabel, + TEXT("preview") + ); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistCubeDeskBound4LeaderboardProjectionTest, + "HyperTwist.CleanRoom.CubeDesk.Bound4.LeaderboardProjection", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistCubeDeskBound4LeaderboardProjectionTest::RunTest(const FString& Parameters) +{ + FHyperTwistTrainingFeatureGovernanceContract FeatureGovernanceContract; + FHyperTwistTrainingEarlyAccessRolloutContract EarlyAccessContract; + TestTrue( + TEXT("Control-plane publication contracts must resolve for leaderboard projection."), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::ResolveControlPlaneContracts( + FeatureGovernanceContract, + EarlyAccessContract + ) + ); + + const FHyperTwistTrainingDeck Deck = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeHypercubeDeck( + TEXT("deck/hypercube-leaderboard") + ); + const FHyperTwistTrainingPublicationBundle Bundle = + UHyperTwistTrainingPublicationLibrary::BuildPublicationBundle( + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeLearnerProfile( + TEXT("bound4-leaderboard-user"), + 5, + 4 + ), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakeDeckHistorySummary( + TEXT("bound4-leaderboard-user"), + Deck.DeckId + ), + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::MakePersonalBestSummary( + TEXT("bound4-leaderboard-user"), + Deck.DeckId + ), + Deck, + UHyperTwistTrainingKnowledgeLibrary::MakeHypercubingKnowledgeReferenceBundle(), + FeatureGovernanceContract, + EarlyAccessContract + ); + + TestTrue(TEXT("The leaderboard bundle must resolve as structurally valid."), Bundle.Leaderboard.IsStructurallyValid()); + TestEqual( + TEXT("The hypercube deck must match exactly two bundled leaderboard events."), + Bundle.Leaderboard.MatchedEventCount, + 2 + ); + TestEqual( + TEXT("The leaderboard projection must materialize a single and an Ao5 row."), + Bundle.Leaderboard.Entries.Num(), + 2 + ); + TestTrue( + TEXT("The leaderboard projection must retain the default rank column."), + Bundle.Leaderboard.ColumnOrder.Contains(TEXT("rank")) + ); + const FHyperTwistTrainingLeaderboardEntry* SingleEntry = + HyperTwistCubeDeskBound4PublicationEntitlementTestInternal::FindEntryByEventId( + Bundle.Leaderboard, + TEXT("event/3x3x3x3/single") + ); + TestNotNull(TEXT("The single-event leaderboard row must exist."), SingleEntry); + if (SingleEntry == nullptr) + { + return false; + } + + TestEqual( + TEXT("The single-event leaderboard row must retain the preferred program badge."), + SingleEntry->ProgramLabel, + TEXT("HSC") + ); + TestTrue( + TEXT("The single-event leaderboard row must expose a formatted metric label."), + !SingleEntry->MetricPrimaryValue.IsEmpty() + ); + TestTrue( + TEXT("Published leaderboard projections must expose published entries."), + Bundle.Leaderboard.bHasPublishedEntries + ); + return true; +} + +#endif