Honor review template launch semantics
This commit is contained in:
parent
d38a074de3
commit
f28f6df06d
2 changed files with 277 additions and 1 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Reference in a new issue