Resume stored review plans after run clear

This commit is contained in:
axiomlogicnexus 2026-05-06 21:12:28 +02:00
parent 0a137fd57c
commit d38a074de3
2 changed files with 251 additions and 2 deletions

View file

@ -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<FString> 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(

View file

@ -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.