Honor review template launch semantics

This commit is contained in:
axiomlogicnexus 2026-05-06 22:25:41 +02:00
parent d38a074de3
commit f28f6df06d
2 changed files with 277 additions and 1 deletions

View file

@ -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<FHyperTwistTrainingCaseRecommendation> BuildTemplateReviewRecommendations(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingDeck& SourceDeck,
const FHyperTwistTrainingDeck& ReviewDeck,
const FString& UserId,
const FHyperTwistTrainingSessionTemplate& SessionTemplate
)
{
const TArray<FHyperTwistTrainingCaseRecommendation> SourceRecommendations =
UHyperTwistTrainingRepositoryLibrary::ListCaseRecommendationsForDeck(
RepositoryState,
SourceDeck,
UserId,
FString(),
SourceDeck.Cases.Num()
);
TMap<FString, FHyperTwistTrainingCaseRecommendation> RecommendationByCaseId;
for (const FHyperTwistTrainingCaseRecommendation& Recommendation : SourceRecommendations)
{
if (Recommendation.IsStructurallyValid())
{
RecommendationByCaseId.Add(Recommendation.TrainingCase.CaseId, Recommendation);
}
}
TArray<FHyperTwistTrainingCaseRecommendation> 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<float>(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<FHyperTwistTrainingCaseRecommendation> 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(

View file

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