Resume stored review plans after run clear
This commit is contained in:
parent
0a137fd57c
commit
d38a074de3
2 changed files with 251 additions and 2 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Reference in a new issue