From d38a074de3d1f968d4a3d5cf503e394b5aec758a Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 6 May 2026 21:12:28 +0200 Subject: [PATCH] Resume stored review plans after run clear --- .../HyperTwistTrainingSubsystem.cpp | 156 +++++++++++++++++- ...RESUMPTION_CONTINUITY_PACKET_2026-05-06.md | 97 +++++++++++ 2 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 docs/arch/HYPERTWIST_PHASE4_POST_CLEAR_REVIEW_RESUMPTION_CONTINUITY_PACKET_2026-05-06.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index e9235a7..3c8281f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -1253,6 +1253,101 @@ namespace HyperTwistTrainingSubsystemInternal return FocusDeck; } + bool IsLiveReviewPlanEntryState(const EHyperTwistTrainingReviewPlanEntryState EntryState) + { + return EntryState == EHyperTwistTrainingReviewPlanEntryState::Current + || EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat + || EntryState == EHyperTwistTrainingReviewPlanEntryState::Pending; + } + + FHyperTwistTrainingDeck BuildReviewDeckFromPlan( + const FHyperTwistTrainingDeck& SourceDeck, + const FHyperTwistTrainingReviewPlanState& ReviewPlan, + int32 MaxCases + ) + { + FHyperTwistTrainingDeck ReviewDeck; + if (!SourceDeck.IsStructurallyValid() || !ReviewPlan.IsStructurallyValid() || ReviewPlan.bCompleted) + { + return ReviewDeck; + } + + ReviewDeck = SourceDeck; + ReviewDeck.DeckId = !ReviewPlan.ReviewDeckId.IsEmpty() + ? ReviewPlan.ReviewDeckId + : SourceDeck.DeckId + TEXT("/review"); + ReviewDeck.Title = SourceDeck.Title + TEXT(" Review"); + ReviewDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Spaced; + ReviewDeck.Tags = SourceDeck.Tags; + ReviewDeck.Tags.AddUnique(TEXT("review")); + ReviewDeck.Tags.AddUnique(TEXT("review-plan-carryover")); + ReviewDeck.Cases.Reset(); + if (ReviewPlan.DeliveryMode != EHyperTwistTrainingDeliveryMode::Timer + && !ReviewDeck.DeliveryModes.Contains(ReviewPlan.DeliveryMode)) + { + ReviewDeck.DeliveryModes.Insert(ReviewPlan.DeliveryMode, 0); + } + + const int32 EffectiveMaxCases = MaxCases > 0 ? MaxCases : ReviewPlan.Entries.Num(); + TSet AddedCaseIds; + const auto TryAddCaseById = [&SourceDeck, &ReviewDeck, &AddedCaseIds, EffectiveMaxCases]( + const FString& CaseId + ) -> bool + { + if (CaseId.IsEmpty() || AddedCaseIds.Contains(CaseId)) + { + return false; + } + + const FHyperTwistTrainingCase* MatchingCase = SourceDeck.Cases.FindByPredicate( + [&CaseId](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.CaseId == CaseId; + } + ); + if (MatchingCase == nullptr) + { + return false; + } + + ReviewDeck.Cases.Add(*MatchingCase); + AddedCaseIds.Add(CaseId); + return ReviewDeck.Cases.Num() >= EffectiveMaxCases; + }; + + int32 CurrentEntryIndex = ReviewPlan.CurrentEntryIndex; + if (!ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex)) + { + CurrentEntryIndex = ReviewPlan.Entries.IndexOfByPredicate( + [](const FHyperTwistTrainingReviewPlanEntry& Entry) + { + return Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current; + } + ); + } + if (ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex) + && IsLiveReviewPlanEntryState(ReviewPlan.Entries[CurrentEntryIndex].EntryState) + && TryAddCaseById(ReviewPlan.Entries[CurrentEntryIndex].CaseId)) + { + return ReviewDeck; + } + + for (const FHyperTwistTrainingReviewPlanEntry& Entry : ReviewPlan.Entries) + { + if (!IsLiveReviewPlanEntryState(Entry.EntryState)) + { + continue; + } + + if (TryAddCaseById(Entry.CaseId)) + { + break; + } + } + + return ReviewDeck.Cases.Num() > 0 ? ReviewDeck : FHyperTwistTrainingDeck(); + } + FHyperTwistCoachBrief BuildCoachBriefFromQueueEntry( const FHyperTwistTrainingCoachSessionQueueState& QueueState, const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry, @@ -3103,17 +3198,45 @@ bool UHyperTwistTrainingSubsystem::TryMaterializeTrainingSessionTemplateForUser( FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveReviewDeck(int32 MaxCases) const { - if (!HasActiveRun()) + FHyperTwistTrainingDeck SourceDeck; + if (HasActiveRun()) + { + SourceDeck = ResolveRepositoryFocusDeck(); + } + else if (ActiveReviewPlanState.IsStructurallyValid()) + { + if (!ActiveReviewPlanState.SourceDeckId.IsEmpty()) + { + UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog( + UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog(), + ActiveReviewPlanState.SourceDeckId, + SourceDeck + ); + } + if (!SourceDeck.IsStructurallyValid()) + { + SourceDeck = ResolveRepositoryFocusDeck(); + } + } + else { return FHyperTwistTrainingDeck(); } - const FHyperTwistTrainingDeck SourceDeck = ResolveRepositoryFocusDeck(); if (!SourceDeck.IsStructurallyValid()) { return FHyperTwistTrainingDeck(); } + if (!HasActiveRun() && ActiveReviewPlanState.IsStructurallyValid()) + { + return HyperTwistTrainingSubsystemInternal::BuildReviewDeckFromPlan( + SourceDeck, + ActiveReviewPlanState, + MaxCases + ); + } + const FHyperTwistTrainingReviewPolicy ReviewPolicy = HyperTwistTrainingSubsystemInternal::ResolveActiveReviewPolicy( ActiveRunState, @@ -3379,6 +3502,35 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartRecommendedReview EHyperTwistTrainingDeliveryMode Mode ) { + if (!HasActiveRun() && ActiveReviewPlanState.IsStructurallyValid()) + { + const FHyperTwistTrainingReviewPlanState StoredReviewPlan = ActiveReviewPlanState; + const FHyperTwistTrainingDeck ReviewDeck = BuildActiveReviewDeck(MaxCases); + if (!ReviewDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingRunState(); + } + + const EHyperTwistTrainingDeliveryMode ResumeMode = StoredReviewPlan.DeliveryMode; + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( + ReviewDeck, + StoredReviewPlan.UserId, + SessionId, + ResumeMode + ); + if (!RunState.IsStructurallyValid()) + { + return RunState; + } + + ActiveReviewPlanState = StoredReviewPlan; + ActiveReviewFlowStatus = UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus( + ActiveReviewPlanState + ); + RefreshRepositoryViews(); + return RunState; + } + const FHyperTwistTrainingDeck SourceDeck = ActiveRunState.ActiveDeck; const FHyperTwistTrainingReviewPolicy ReviewPolicy = HyperTwistTrainingSubsystemInternal::ResolveActiveReviewPolicy( diff --git a/docs/arch/HYPERTWIST_PHASE4_POST_CLEAR_REVIEW_RESUMPTION_CONTINUITY_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE4_POST_CLEAR_REVIEW_RESUMPTION_CONTINUITY_PACKET_2026-05-06.md new file mode 100644 index 0000000..c2808e8 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_POST_CLEAR_REVIEW_RESUMPTION_CONTINUITY_PACKET_2026-05-06.md @@ -0,0 +1,97 @@ +# HyperTwist Phase 4 post-clear review resumption continuity packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` coach-to-review validation slice + +## Purpose + +This packet makes a repository-backed pending review plan resumable after `ClearActiveRun()` instead of merely inspectable. + +The open tasks are: + +- let a rehydrated idle review plan materialize a truthful carryover review deck after the active run has been cleared +- let the owned review-run start path resume that stored review plan instead of only starting brand-new review plans from a still-active run + +It is not: + +- a new review-plan persistence packet +- a new queue-state packet +- a broader dashboard command-surface packet +- a rewrite of review-policy selection + +## Scope + +Bounded lane: + +- build a carryover review deck directly from `ActiveReviewPlanState` when no run is active and the stored review plan is still open +- preserve current-entry / needs-repeat / pending ordering so review-plan mutation stays aligned with the launched deck +- let `StartRecommendedReviewRun()` resume an already-stored review plan from idle state without overwriting it with a brand-new plan + +Out of scope: + +- changing how review plans are scored or finalized +- changing coach queue priorities +- adding a new dedicated dashboard button surface +- widening into unrelated consumer/UI work + +## Why this was the right next packet + +Before this slice: + +- idle review-plan rehydration after clear was landed +- post-clear coach action-plan continuity was landed +- dashboard and queue idle-closure slices were landed + +But one operational seam still remained: + +- after clear, the stored review plan could be rehydrated for inspection +- but `BuildActiveReviewDeck()` only worked from an active run +- and `StartRecommendedReviewRun()` only knew how to build a fresh review plan from an active run instead of resuming the stored one + +That meant: + +- carryover review state could survive in repository state +- while the owned runtime lane still lacked a direct way to turn that stored plan back into the next review run + +So the next honest move was: + +- let the stored plan materialize its own carryover deck +- and let the existing review-run start path resume that stored plan from idle state + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp` + - added a review-plan carryover deck builder that materializes live review cases from non-terminal review-plan entries + - `BuildActiveReviewDeck()` now works from a rehydrated stored review plan when no run is active + - `StartRecommendedReviewRun()` now resumes an already-stored review plan from idle state instead of always building a new plan from an active run + +## Product effect + +The post-clear review lane is now more complete: + +- a stored carryover review plan can now produce the same next review deck after clear that it would have produced before clear +- the owned runtime entry point can resume that stored plan directly +- review-plan mutation during the resumed run stays aligned with the launched carryover cases instead of forcing a brand-new review-plan branch + +## Acceptance criteria + +- when no run is active and `ActiveReviewPlanState` is still open, `BuildActiveReviewDeck()` returns a structurally valid carryover review deck +- that deck preserves the current/repeat/pending review-plan ordering +- `StartRecommendedReviewRun()` can resume the stored review plan from idle state without overwriting it with a new plan +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm `BuildActiveReviewDeck()` works with a rehydrated idle review plan +3. confirm `StartRecommendedReviewRun()` resumes the stored plan from idle state +4. confirm resumed attempts still mutate the same review plan rather than creating a new one +5. confirm no broader queue/dashboard redesign was reopened + +That is the packet.