diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 3c8281f..e9d1b58 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -1348,6 +1348,118 @@ namespace HyperTwistTrainingSubsystemInternal return ReviewDeck.Cases.Num() > 0 ? ReviewDeck : FHyperTwistTrainingDeck(); } + bool ShouldResumeStoredReviewPlanFromTemplate( + const FHyperTwistTrainingSessionTemplate& SessionTemplate, + const FHyperTwistTrainingReviewPlanState& ReviewPlan, + const FString& UserId + ) + { + if (!SessionTemplate.bPreferReviewRun + || !ReviewPlan.IsStructurallyValid() + || ReviewPlan.bCompleted) + { + return false; + } + if (!UserId.IsEmpty() && ReviewPlan.UserId != UserId) + { + return false; + } + if (!SessionTemplate.SourceReviewPlanId.IsEmpty() + && SessionTemplate.SourceReviewPlanId != ReviewPlan.PlanId) + { + return false; + } + if (!SessionTemplate.FocusDeckId.IsEmpty() + && SessionTemplate.FocusDeckId != ReviewPlan.SourceDeckId) + { + return false; + } + + return true; + } + + FHyperTwistTrainingReviewPolicy BuildTemplateReviewPolicy( + const FHyperTwistTrainingSessionTemplate& SessionTemplate, + int32 CaseCount + ) + { + FHyperTwistTrainingReviewPolicy ReviewPolicy = + UHyperTwistTrainingRepositoryLibrary::GetDefaultReviewPolicy(SessionTemplate.SuggestedMode); + ReviewPolicy.MaxCases = CaseCount > 0 ? CaseCount : ReviewPolicy.MaxCases; + ReviewPolicy.bPreferShortReviewSets = + ReviewPolicy.bPreferShortReviewSets + || SessionTemplate.bPreferShortReviewSet + || (CaseCount > 0 && CaseCount <= 4); + return ReviewPolicy; + } + + TArray BuildTemplateReviewRecommendations( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingDeck& SourceDeck, + const FHyperTwistTrainingDeck& ReviewDeck, + const FString& UserId, + const FHyperTwistTrainingSessionTemplate& SessionTemplate + ) + { + const TArray SourceRecommendations = + UHyperTwistTrainingRepositoryLibrary::ListCaseRecommendationsForDeck( + RepositoryState, + SourceDeck, + UserId, + FString(), + SourceDeck.Cases.Num() + ); + TMap RecommendationByCaseId; + for (const FHyperTwistTrainingCaseRecommendation& Recommendation : SourceRecommendations) + { + if (Recommendation.IsStructurallyValid()) + { + RecommendationByCaseId.Add(Recommendation.TrainingCase.CaseId, Recommendation); + } + } + + TArray OrderedRecommendations; + OrderedRecommendations.Reserve(ReviewDeck.Cases.Num()); + const FString DefaultReason = !SessionTemplate.GuidanceLabel.IsEmpty() + ? SessionTemplate.GuidanceLabel + : (SessionTemplate.bCoachReview ? TEXT("coach-review-template") : TEXT("review-template")); + const FString ReferenceUtc = !SessionTemplate.LastUpdatedAtUtc.IsEmpty() + ? SessionTemplate.LastUpdatedAtUtc + : FDateTime::UtcNow().ToIso8601(); + for (int32 CaseIndex = 0; CaseIndex < ReviewDeck.Cases.Num(); ++CaseIndex) + { + const FHyperTwistTrainingCase& TrainingCase = ReviewDeck.Cases[CaseIndex]; + if (!TrainingCase.IsStructurallyValid()) + { + continue; + } + + FHyperTwistTrainingCaseRecommendation OrderedRecommendation; + if (const FHyperTwistTrainingCaseRecommendation* MatchingRecommendation = + RecommendationByCaseId.Find(TrainingCase.CaseId)) + { + OrderedRecommendation = *MatchingRecommendation; + } + + OrderedRecommendation.TrainingCase = TrainingCase; + if (OrderedRecommendation.ReferenceUtc.IsEmpty()) + { + OrderedRecommendation.ReferenceUtc = ReferenceUtc; + } + if (OrderedRecommendation.RecommendationReason.IsEmpty()) + { + OrderedRecommendation.RecommendationReason = DefaultReason; + } + if (OrderedRecommendation.RecommendationScore <= -999.0f) + { + OrderedRecommendation.RecommendationScore = 1000.0f - static_cast(CaseIndex); + } + OrderedRecommendations.Add(OrderedRecommendation); + } + + return OrderedRecommendations; + } + FHyperTwistCoachBrief BuildCoachBriefFromQueueEntry( const FHyperTwistTrainingCoachSessionQueueState& QueueState, const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry, @@ -3450,12 +3562,79 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromTe return FHyperTwistTrainingRunState(); } - return StartTrainingRunFromDeck( + if (HyperTwistTrainingSubsystemInternal::ShouldResumeStoredReviewPlanFromTemplate( + *MatchingTemplate, + ActiveReviewPlanState, + ResolvedUserId)) + { + return StartRecommendedReviewRun( + SessionId, + MaterializedDeck.Cases.Num(), + MatchingTemplate->SuggestedMode + ); + } + + FHyperTwistTrainingDeck SourceDeck; + FHyperTwistTrainingReviewPolicy ReviewPolicy; + TArray TemplateRecommendations; + if (MatchingTemplate->bPreferReviewRun + && UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog( + UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog(), + MatchingTemplate->FocusDeckId, + SourceDeck)) + { + ReviewPolicy = HyperTwistTrainingSubsystemInternal::BuildTemplateReviewPolicy( + *MatchingTemplate, + MaterializedDeck.Cases.Num() + ); + TemplateRecommendations = + HyperTwistTrainingSubsystemInternal::BuildTemplateReviewRecommendations( + TrainingRepositoryState, + SourceDeck, + MaterializedDeck, + ResolvedUserId, + *MatchingTemplate + ); + } + + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( MaterializedDeck, ResolvedUserId, SessionId, MatchingTemplate->SuggestedMode ); + if (!RunState.IsStructurallyValid() + || !MatchingTemplate->bPreferReviewRun + || !SourceDeck.IsStructurallyValid()) + { + return RunState; + } + + ActiveReviewPlanState = UHyperTwistTrainingRepositoryLibrary::BuildReviewPlan( + SourceDeck, + MaterializedDeck, + ResolvedUserId, + TemplateRecommendations, + ReviewPolicy, + FString::Printf(TEXT("review_plan_%s"), *RunState.Session.TrainingSessionId), + RunState.Session.StartedAtUtc, + MatchingTemplate->SuggestedMode + ); + if (ActiveReviewPlanState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertReviewPlan( + TrainingRepositoryState, + ActiveReviewPlanState + ); + ActiveReviewFlowStatus = UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(ActiveReviewPlanState); + RefreshRepositoryViews(); + } + else + { + ActiveReviewFlowStatus = FHyperTwistTrainingReviewFlowStatus(); + } + + return RunState; } FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartActiveMethodDrillFollowUpRun( diff --git a/docs/arch/HYPERTWIST_PHASE4_REVIEW_TEMPLATE_LAUNCH_CONTINUITY_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE4_REVIEW_TEMPLATE_LAUNCH_CONTINUITY_PACKET_2026-05-06.md new file mode 100644 index 0000000..0785177 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_REVIEW_TEMPLATE_LAUNCH_CONTINUITY_PACKET_2026-05-06.md @@ -0,0 +1,97 @@ +# HyperTwist Phase 4 review template launch continuity packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` review-template continuity slice + +## Purpose + +This packet makes review-preferring session templates launch through actual review-plan semantics instead of degrading into plain deck runs. + +The open tasks are: + +- let repository-backed review templates resume an already-stored pending review plan when one is still open +- let other review-preferring templates create and persist a fresh review plan for the launched template deck + +It is not: + +- a new dashboard button packet +- a broader session-template consumer packet +- a method-drill packet +- a review-policy redesign + +## Scope + +Bounded lane: + +- keep `StartTrainingRunFromTemplate()` on its existing thin launch surface +- detect when the selected template explicitly prefers review execution +- resume `ActiveReviewPlanState` through `StartRecommendedReviewRun()` when the template matches an open stored review plan +- otherwise build and persist a new review plan from the launched template deck so template-backed review runs stay visible in repository review state + +Out of scope: + +- changing queue / coach action ordering +- adding new template browsers or launch controls +- widening into method-drill follow-up launch semantics +- changing how review recommendations are scored outside the launched template deck + +## Why this was the right next packet + +Before this slice: + +- stored pending review plans could already be resumed directly after `ClearActiveRun()` +- repository state already exposed review-preferring templates, including `Resume Review Plan` +- template materialization already tagged review-preferring template decks correctly + +But one execution seam still remained: + +- `StartTrainingRunFromTemplate()` ignored `bPreferReviewRun` +- review-preferring templates therefore started plain training runs +- and those launches did not resume or persist review-plan state + +That meant: + +- the direct review-resumption runtime path was coherent +- while the template-backed review entry path could still silently drop review semantics + +So the next honest move was: + +- make review-preferring template launches reuse the stored review plan when appropriate +- and otherwise persist a fresh review plan for the launched template deck + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp` + - added review-template launch helpers that detect when a stored review plan should be resumed + - `StartTrainingRunFromTemplate()` now routes matching review-preferring templates through `StartRecommendedReviewRun()` + - review-preferring templates without a resumable stored plan now build and upsert a fresh review plan after the template deck launch + +## Product effect + +Template-backed review entry is now more truthful: + +- `Resume Review Plan` templates no longer fall back to a plain deck run when the stored plan is still open +- review-preferring template launches now keep repository-backed review state aligned with the run that just started +- template-based review continuation no longer loses `ActiveReviewPlanState` / `ActiveReviewFlowStatus` continuity at launch time + +## Acceptance criteria + +- `StartTrainingRunFromTemplate()` resumes a matching stored review plan when the template prefers review execution and an open plan is already present +- review-preferring templates without a resumable stored plan persist a fresh review plan after launch +- non-review-preferring templates keep their existing plain run behavior +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm `StartTrainingRunFromTemplate()` resumes stored review-plan state for `Resume Review Plan` +3. confirm review-preferring templates without a resumable plan upsert a new review plan after launch +4. confirm non-review templates still launch as ordinary training runs + +That is the packet.