Harden training repository integrity flows

This commit is contained in:
axiomlogicnexus 2026-04-25 22:08:09 +02:00
parent e0a3057091
commit 6a212cfef8
10 changed files with 774 additions and 45 deletions

View file

@ -437,6 +437,40 @@ FHyperTwistTrainingRepositoryState UHyperTwistContractLibrary::MakeSampleTrainin
return RepositoryState;
}
FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistContractLibrary::MakeSampleTrainingRepositoryIntegrityReport()
{
FHyperTwistTrainingRepositoryState RepositoryState = MakeSampleTrainingRepositoryState();
if (RepositoryState.RunRecords.Num() > 0)
{
RepositoryState.RunRecords.Add(RepositoryState.RunRecords[0]);
}
RepositoryState.RunRecords.Add(FHyperTwistTrainingRunRecord());
if (RepositoryState.LearnerStates.Num() > 0)
{
RepositoryState.LearnerStates.Add(RepositoryState.LearnerStates[0]);
}
RepositoryState.LearnerStates.Add(FHyperTwistTrainingCaseLearnerState());
const FHyperTwistTrainingReviewPlanState ReviewPlanState = MakeSampleTrainingReviewPlanState();
if (ReviewPlanState.IsStructurallyValid())
{
RepositoryState.ReviewPlans.Add(ReviewPlanState);
RepositoryState.ReviewPlans.Add(ReviewPlanState);
}
RepositoryState.ReviewPlans.Add(FHyperTwistTrainingReviewPlanState());
const TArray<FHyperTwistTrainingSessionTemplate> SessionTemplates = MakeSampleTrainingSessionTemplates();
if (SessionTemplates.Num() > 0)
{
RepositoryState.StoredSessionTemplates.Add(SessionTemplates[0]);
RepositoryState.StoredSessionTemplates.Add(SessionTemplates[0]);
}
RepositoryState.StoredSessionTemplates.Add(FHyperTwistTrainingSessionTemplate());
return UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState);
}
FHyperTwistTrainingDeckHistorySummary UHyperTwistContractLibrary::MakeSampleTrainingDeckHistorySummary()
{
const FHyperTwistTrainingRunStepResult StepResult = MakeSampleTrainingRunStepResult();
@ -501,6 +535,25 @@ FHyperTwistTrainingTimerExportPacket UHyperTwistContractLibrary::MakeSampleTrain
);
}
FHyperTwistTrainingTimerExportIntegrityReport UHyperTwistContractLibrary::MakeSampleTrainingTimerExportIntegrityReport()
{
FHyperTwistTrainingTimerExportPacket TimerExportPacket = MakeSampleTrainingTimerExportPacket();
TimerExportPacket.FormatVersion.Reset();
TimerExportPacket.ExportedAtUtc.Reset();
if (TimerExportPacket.Entries.Num() > 0)
{
TimerExportPacket.Entries.Add(TimerExportPacket.Entries[0]);
FHyperTwistTrainingTimerExportEntry MismatchedEntry = TimerExportPacket.Entries[0];
MismatchedEntry.AttemptId += TEXT("-mismatch");
MismatchedEntry.UserId = TEXT("other-user");
TimerExportPacket.Entries.Add(MismatchedEntry);
}
TimerExportPacket.Entries.Add(FHyperTwistTrainingTimerExportEntry());
return UHyperTwistTrainingRepositoryLibrary::DeriveTimerExportIntegrityReport(TimerExportPacket);
}
FHyperTwistTrainingCaseScopedStats UHyperTwistContractLibrary::MakeSampleTrainingCaseScopedStats()
{
const FHyperTwistTrainingRunStepResult StepResult = MakeSampleTrainingRunStepResult();

View file

@ -1,6 +1,7 @@
#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HAL/FileManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
@ -93,7 +94,11 @@ bool UHyperTwistTrainingPersistenceLibrary::SaveTrainingRepositoryStateToFile(
IFileManager::Get().MakeDirectory(*Directory, true);
}
const FString Json = UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(RepositoryState);
const FHyperTwistTrainingRepositoryState RepairedRepositoryState =
UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(RepositoryState);
const FString Json = UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(
RepairedRepositoryState
);
return !Json.IsEmpty() && FFileHelper::SaveStringToFile(Json, *OutResolvedPath);
}
@ -103,9 +108,11 @@ bool UHyperTwistTrainingPersistenceLibrary::SaveTrainingTimerExportPacketToFile(
FString& OutResolvedPath
)
{
const FHyperTwistTrainingTimerExportPacket NormalizedPacket =
UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(TimerExportPacket);
OutResolvedPath = ResolveTrainingTimerExportPath(
TimerExportPacket.UserId,
TimerExportPacket.DeckId,
NormalizedPacket.UserId,
NormalizedPacket.DeckId,
ExportPath
);
const FString Directory = FPaths::GetPath(OutResolvedPath);
@ -114,7 +121,7 @@ bool UHyperTwistTrainingPersistenceLibrary::SaveTrainingTimerExportPacketToFile(
IFileManager::Get().MakeDirectory(*Directory, true);
}
const FString Json = UHyperTwistContractLibrary::SerializeTrainingTimerExportPacketToJson(TimerExportPacket);
const FString Json = UHyperTwistContractLibrary::SerializeTrainingTimerExportPacketToJson(NormalizedPacket);
return !Json.IsEmpty() && FFileHelper::SaveStringToFile(Json, *OutResolvedPath);
}
@ -133,7 +140,13 @@ bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingRepositoryStateFromFile(
return false;
}
return UHyperTwistContractLibrary::DeserializeTrainingRepositoryStateFromJson(Json, OutRepositoryState);
if (!UHyperTwistContractLibrary::DeserializeTrainingRepositoryStateFromJson(Json, OutRepositoryState))
{
return false;
}
OutRepositoryState = UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(OutRepositoryState);
return true;
}
bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingTimerExportPacketFromFile(
@ -153,5 +166,13 @@ bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingTimerExportPacketFromFil
return false;
}
return UHyperTwistContractLibrary::DeserializeTrainingTimerExportPacketFromJson(Json, OutTimerExportPacket);
if (!UHyperTwistContractLibrary::DeserializeTrainingTimerExportPacketFromJson(Json, OutTimerExportPacket))
{
return false;
}
OutTimerExportPacket = UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(
OutTimerExportPacket
);
return true;
}

View file

@ -277,6 +277,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return UserId + CaseKeySeparator + DeckId + CaseKeySeparator + CaseId;
}
FString MakeTemplateKey(const FString& UserId, const FString& TemplateId)
{
return UserId + CaseKeySeparator + TemplateId;
}
bool SplitCaseKey(const FString& CaseKey, FString& OutUserId, FString& OutDeckId, FString& OutCaseId)
{
TArray<FString> Parts;
@ -295,6 +300,21 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return true;
}
void AppendRepairAction(TArray<FString>& RepairActions, const FString& Action)
{
if (!Action.IsEmpty())
{
RepairActions.AddUnique(Action);
}
}
FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory(
const FString& UserId,
const FString& DeckId,
const FString& CaseId,
const TArray<FHyperTwistTrainingReviewHistoryEntry>& ReviewHistory
);
FHyperTwistTrainingReviewHistoryEntry MakeReviewHistoryEntryFromAttemptHistory(
const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry,
const TArray<FHyperTwistTrainingReviewHistoryEntry>& ExistingReviewHistory
@ -382,6 +402,49 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return ReviewHistoryByCase;
}
TArray<FHyperTwistTrainingCaseLearnerState> DeriveLearnerStatesFromRunRecords(
const TArray<FHyperTwistTrainingRunRecord>& RunRecords
)
{
TArray<FHyperTwistTrainingCaseLearnerState> LearnerStates;
const TMap<FString, TArray<FHyperTwistTrainingReviewHistoryEntry>> ReviewHistoryByCase =
BuildReviewHistoryByCase(RunRecords);
for (const TPair<FString, TArray<FHyperTwistTrainingReviewHistoryEntry>>& Pair : ReviewHistoryByCase)
{
FString UserId;
FString DeckId;
FString CaseId;
if (!SplitCaseKey(Pair.Key, UserId, DeckId, CaseId))
{
continue;
}
FHyperTwistTrainingCaseLearnerState LearnerState =
DeriveLearnerStateFromReviewHistory(UserId, DeckId, CaseId, Pair.Value);
if (LearnerState.IsStructurallyValid())
{
LearnerStates.Add(LearnerState);
}
}
LearnerStates.Sort([](
const FHyperTwistTrainingCaseLearnerState& Left,
const FHyperTwistTrainingCaseLearnerState& Right
)
{
if (Left.UserId != Right.UserId)
{
return Left.UserId < Right.UserId;
}
if (Left.DeckId != Right.DeckId)
{
return Left.DeckId < Right.DeckId;
}
return Left.CaseId < Right.CaseId;
});
return LearnerStates;
}
FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory(
const FString& UserId,
const FString& DeckId,
@ -1812,7 +1875,7 @@ FHyperTwistTrainingTimerExportPacket UHyperTwistTrainingRepositoryLibrary::Expor
}
}
return TimerExportPacket;
return NormalizeTimerExportPacket(TimerExportPacket);
}
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::ImportTimerResults(
@ -1821,13 +1884,14 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::ImportT
)
{
FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState;
if (!TimerExportPacket.IsStructurallyValid())
const FHyperTwistTrainingTimerExportPacket NormalizedPacket = NormalizeTimerExportPacket(TimerExportPacket);
if (!NormalizedPacket.IsStructurallyValid())
{
return UpdatedRepositoryState;
}
TMap<FString, TArray<FHyperTwistTrainingTimerExportEntry>> EntriesBySessionId;
for (const FHyperTwistTrainingTimerExportEntry& TimerExportEntry : TimerExportPacket.Entries)
for (const FHyperTwistTrainingTimerExportEntry& TimerExportEntry : NormalizedPacket.Entries)
{
if (!TimerExportEntry.IsStructurallyValid())
{
@ -1848,15 +1912,470 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::ImportT
FHyperTwistTrainingRunRecord ImportedRunRecord =
HyperTwistTrainingRepositoryLibraryInternal::BuildRunRecordFromTimerExportEntries(
TimerExportPacket.UserId,
TimerExportPacket.DeckId,
NormalizedPacket.UserId,
NormalizedPacket.DeckId,
SessionId,
SessionEntries
);
UpdatedRepositoryState = UpsertRunRecord(UpdatedRepositoryState, ImportedRunRecord);
}
return UpdatedRepositoryState;
return RepairRepositoryState(UpdatedRepositoryState);
}
FHyperTwistTrainingTimerExportIntegrityReport UHyperTwistTrainingRepositoryLibrary::DeriveTimerExportIntegrityReport(
const FHyperTwistTrainingTimerExportPacket& TimerExportPacket
)
{
FHyperTwistTrainingTimerExportIntegrityReport Report;
Report.UserId = TimerExportPacket.UserId;
Report.DeckId = TimerExportPacket.DeckId;
Report.TotalEntryCount = TimerExportPacket.Entries.Num();
if (TimerExportPacket.FormatVersion.IsEmpty())
{
Report.MissingPacketMetadataCount += 1;
}
if (TimerExportPacket.SourceSystem.IsEmpty())
{
Report.MissingPacketMetadataCount += 1;
}
if (TimerExportPacket.UserId.IsEmpty())
{
Report.MissingPacketMetadataCount += 1;
}
if (TimerExportPacket.DeckId.IsEmpty())
{
Report.MissingPacketMetadataCount += 1;
}
if (TimerExportPacket.ExportedAtUtc.IsEmpty())
{
Report.MissingPacketMetadataCount += 1;
}
TSet<FString> SeenAttemptIds;
for (const FHyperTwistTrainingTimerExportEntry& Entry : TimerExportPacket.Entries)
{
if (!Entry.IsStructurallyValid())
{
Report.InvalidEntryCount += 1;
continue;
}
if (SeenAttemptIds.Contains(Entry.AttemptId))
{
Report.DuplicateEntryCount += 1;
}
else
{
SeenAttemptIds.Add(Entry.AttemptId);
}
const bool bScopeMismatch =
(!TimerExportPacket.UserId.IsEmpty() && Entry.UserId != TimerExportPacket.UserId)
|| (!TimerExportPacket.DeckId.IsEmpty() && Entry.DeckId != TimerExportPacket.DeckId);
if (bScopeMismatch)
{
Report.MismatchedScopeEntryCount += 1;
}
}
if (Report.MissingPacketMetadataCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("fill-missing-timer-export-metadata")
);
}
if (Report.InvalidEntryCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-timer-export-entries")
);
}
if (Report.DuplicateEntryCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("deduplicate-timer-export-attempt-ids")
);
}
if (Report.MismatchedScopeEntryCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-mismatched-timer-export-scope-entries")
);
}
return Report;
}
FHyperTwistTrainingTimerExportPacket UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(
const FHyperTwistTrainingTimerExportPacket& TimerExportPacket
)
{
FHyperTwistTrainingTimerExportPacket NormalizedPacket = TimerExportPacket;
if (NormalizedPacket.FormatVersion.IsEmpty())
{
NormalizedPacket.FormatVersion = TEXT("ht-timer-export/v1");
}
if (NormalizedPacket.SourceSystem.IsEmpty())
{
NormalizedPacket.SourceSystem = TEXT("HyperTwistTraining");
}
if (NormalizedPacket.ExportedAtUtc.IsEmpty())
{
NormalizedPacket.ExportedAtUtc = FDateTime::UtcNow().ToIso8601();
}
FString ScopedUserId = NormalizedPacket.UserId;
FString ScopedDeckId = NormalizedPacket.DeckId;
if (ScopedUserId.IsEmpty() || ScopedDeckId.IsEmpty())
{
for (const FHyperTwistTrainingTimerExportEntry& Entry : TimerExportPacket.Entries)
{
if (!Entry.IsStructurallyValid())
{
continue;
}
if (ScopedUserId.IsEmpty())
{
ScopedUserId = Entry.UserId;
}
if (ScopedDeckId.IsEmpty())
{
ScopedDeckId = Entry.DeckId;
}
if (!ScopedUserId.IsEmpty() && !ScopedDeckId.IsEmpty())
{
break;
}
}
}
NormalizedPacket.UserId = ScopedUserId;
NormalizedPacket.DeckId = ScopedDeckId;
NormalizedPacket.Entries.Reset();
TSet<FString> SeenAttemptIds;
for (const FHyperTwistTrainingTimerExportEntry& Entry : TimerExportPacket.Entries)
{
if (!Entry.IsStructurallyValid())
{
continue;
}
if (!ScopedUserId.IsEmpty() && Entry.UserId != ScopedUserId)
{
continue;
}
if (!ScopedDeckId.IsEmpty() && Entry.DeckId != ScopedDeckId)
{
continue;
}
if (SeenAttemptIds.Contains(Entry.AttemptId))
{
continue;
}
SeenAttemptIds.Add(Entry.AttemptId);
NormalizedPacket.Entries.Add(Entry);
}
return NormalizedPacket;
}
FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(
const FHyperTwistTrainingRepositoryState& RepositoryState
)
{
FHyperTwistTrainingRepositoryIntegrityReport Report;
Report.TotalRunRecordCount = RepositoryState.RunRecords.Num();
TSet<FString> SeenRunRecordIds;
for (const FHyperTwistTrainingRunRecord& RunRecord : RepositoryState.RunRecords)
{
if (!RunRecord.IsStructurallyValid())
{
Report.InvalidRunRecordCount += 1;
continue;
}
if (SeenRunRecordIds.Contains(RunRecord.Session.TrainingSessionId))
{
Report.DuplicateRunRecordCount += 1;
continue;
}
SeenRunRecordIds.Add(RunRecord.Session.TrainingSessionId);
}
Report.TotalLearnerStateCount = RepositoryState.LearnerStates.Num();
TSet<FString> SeenLearnerStateKeys;
for (const FHyperTwistTrainingCaseLearnerState& LearnerState : RepositoryState.LearnerStates)
{
if (!LearnerState.IsStructurallyValid())
{
Report.InvalidLearnerStateCount += 1;
continue;
}
const FString LearnerStateKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCaseKey(
LearnerState.UserId,
LearnerState.DeckId,
LearnerState.CaseId
);
if (SeenLearnerStateKeys.Contains(LearnerStateKey))
{
Report.DuplicateLearnerStateCount += 1;
continue;
}
SeenLearnerStateKeys.Add(LearnerStateKey);
}
Report.TotalReviewPlanCount = RepositoryState.ReviewPlans.Num();
TSet<FString> SeenReviewPlanIds;
for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
{
if (!ReviewPlan.IsStructurallyValid())
{
Report.InvalidReviewPlanCount += 1;
continue;
}
if (SeenReviewPlanIds.Contains(ReviewPlan.PlanId))
{
Report.DuplicateReviewPlanCount += 1;
continue;
}
SeenReviewPlanIds.Add(ReviewPlan.PlanId);
}
Report.TotalStoredSessionTemplateCount = RepositoryState.StoredSessionTemplates.Num();
TSet<FString> SeenStoredTemplateKeys;
for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : RepositoryState.StoredSessionTemplates)
{
if (!SessionTemplate.IsStructurallyValid())
{
Report.InvalidStoredSessionTemplateCount += 1;
continue;
}
const FString TemplateKey = HyperTwistTrainingRepositoryLibraryInternal::MakeTemplateKey(
SessionTemplate.UserId,
SessionTemplate.TemplateId
);
if (SeenStoredTemplateKeys.Contains(TemplateKey))
{
Report.DuplicateStoredSessionTemplateCount += 1;
continue;
}
SeenStoredTemplateKeys.Add(TemplateKey);
}
if (Report.InvalidRunRecordCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-run-records")
);
}
if (Report.DuplicateRunRecordCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("deduplicate-run-records-by-session-id")
);
}
if (Report.TotalRunRecordCount > 0
&& (Report.TotalLearnerStateCount == 0
|| Report.InvalidLearnerStateCount > 0
|| Report.DuplicateLearnerStateCount > 0))
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("rebuild-learner-states-from-run-records")
);
}
else if (Report.InvalidLearnerStateCount > 0 || Report.DuplicateLearnerStateCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("filter-and-deduplicate-learner-states")
);
}
if (Report.InvalidReviewPlanCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-review-plans")
);
}
if (Report.DuplicateReviewPlanCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("deduplicate-review-plans-by-plan-id")
);
}
if (Report.InvalidStoredSessionTemplateCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-stored-session-templates")
);
}
if (Report.DuplicateStoredSessionTemplateCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("normalize-and-deduplicate-stored-session-templates")
);
}
return Report;
}
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(
const FHyperTwistTrainingRepositoryState& RepositoryState
)
{
FHyperTwistTrainingRepositoryState RepairedState;
TMap<FString, int32> RunRecordIndexBySessionId;
for (const FHyperTwistTrainingRunRecord& RunRecord : RepositoryState.RunRecords)
{
if (!RunRecord.IsStructurallyValid())
{
continue;
}
const FString& SessionId = RunRecord.Session.TrainingSessionId;
if (const int32* ExistingIndex = RunRecordIndexBySessionId.Find(SessionId))
{
RepairedState.RunRecords[*ExistingIndex] = RunRecord;
}
else
{
RunRecordIndexBySessionId.Add(SessionId, RepairedState.RunRecords.Add(RunRecord));
}
}
RepairedState.RunRecords = HyperTwistTrainingRepositoryLibraryInternal::SortRunRecordsByUpdateUtc(
RepairedState.RunRecords
);
if (RepairedState.RunRecords.Num() > 0)
{
RepairedState.LearnerStates = HyperTwistTrainingRepositoryLibraryInternal::DeriveLearnerStatesFromRunRecords(
RepairedState.RunRecords
);
}
else
{
TMap<FString, int32> LearnerStateIndexByKey;
for (const FHyperTwistTrainingCaseLearnerState& LearnerState : RepositoryState.LearnerStates)
{
if (!LearnerState.IsStructurallyValid())
{
continue;
}
const FString LearnerStateKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCaseKey(
LearnerState.UserId,
LearnerState.DeckId,
LearnerState.CaseId
);
if (const int32* ExistingIndex = LearnerStateIndexByKey.Find(LearnerStateKey))
{
RepairedState.LearnerStates[*ExistingIndex] = LearnerState;
}
else
{
LearnerStateIndexByKey.Add(LearnerStateKey, RepairedState.LearnerStates.Add(LearnerState));
}
}
}
TMap<FString, int32> ReviewPlanIndexById;
for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
{
if (!ReviewPlan.IsStructurallyValid())
{
continue;
}
if (const int32* ExistingIndex = ReviewPlanIndexById.Find(ReviewPlan.PlanId))
{
RepairedState.ReviewPlans[*ExistingIndex] = ReviewPlan;
}
else
{
ReviewPlanIndexById.Add(ReviewPlan.PlanId, RepairedState.ReviewPlans.Add(ReviewPlan));
}
}
TMap<FString, int32> StoredTemplateIndexByKey;
for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : RepositoryState.StoredSessionTemplates)
{
if (!SessionTemplate.IsStructurallyValid())
{
continue;
}
const FHyperTwistTrainingSessionTemplate NormalizedTemplate =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeStoredTemplate(SessionTemplate);
if (!NormalizedTemplate.IsStructurallyValid())
{
continue;
}
const FString TemplateKey = HyperTwistTrainingRepositoryLibraryInternal::MakeTemplateKey(
NormalizedTemplate.UserId,
NormalizedTemplate.TemplateId
);
if (const int32* ExistingIndex = StoredTemplateIndexByKey.Find(TemplateKey))
{
RepairedState.StoredSessionTemplates[*ExistingIndex] = NormalizedTemplate;
}
else
{
StoredTemplateIndexByKey.Add(
TemplateKey,
RepairedState.StoredSessionTemplates.Add(NormalizedTemplate)
);
}
}
RepairedState.StoredSessionTemplates.Sort([](
const FHyperTwistTrainingSessionTemplate& Left,
const FHyperTwistTrainingSessionTemplate& Right
)
{
if (Left.UserId != Right.UserId)
{
return Left.UserId < Right.UserId;
}
if (Left.bUserCreated != Right.bUserCreated)
{
return Left.bUserCreated && !Right.bUserCreated;
}
const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
Left.LastUpdatedAtUtc,
Right.LastUpdatedAtUtc
);
if (UtcCompare != 0)
{
return UtcCompare > 0;
}
return Left.TemplateId < Right.TemplateId;
});
return RepairedState;
}
FHyperTwistTrainingCaseScopedStats UHyperTwistTrainingRepositoryLibrary::DeriveCaseScopedStats(

View file

@ -68,6 +68,18 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRuntimeLibrary::GetTrainin
return FHyperTwistTrainingRepositoryState();
}
FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRepositoryIntegrityReport(
UObject* WorldContextObject
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveRepositoryIntegrityReport();
}
return FHyperTwistTrainingRepositoryIntegrityReport();
}
FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))

View file

@ -25,6 +25,11 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingSubsystem::GetTrainingRepo
return TrainingRepositoryState;
}
FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingSubsystem::GetActiveRepositoryIntegrityReport() const
{
return ActiveRepositoryIntegrityReport;
}
FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingSubsystem::GetActiveDeckHistorySummary() const
{
return ActiveDeckHistorySummary;
@ -114,11 +119,13 @@ FHyperTwistTrainingTimerExportPacket UHyperTwistTrainingSubsystem::BuildTraining
const FString& ReferenceUtc
) const
{
return UHyperTwistTrainingRepositoryLibrary::ExportTimerResultsForDeck(
TrainingRepositoryState,
UserId,
DeckId,
ReferenceUtc
return UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(
UHyperTwistTrainingRepositoryLibrary::ExportTimerResultsForDeck(
TrainingRepositoryState,
UserId,
DeckId,
ReferenceUtc
)
);
}
@ -189,10 +196,7 @@ bool UHyperTwistTrainingSubsystem::SaveTrainingSessionTemplate(
TrainingRepositoryState,
SessionTemplate
);
if (ActiveRunState.Session.UserId == SessionTemplate.UserId)
{
RefreshRepositoryViews();
}
RefreshRepositoryViews();
return true;
}
@ -212,10 +216,7 @@ bool UHyperTwistTrainingSubsystem::RemoveTrainingSessionTemplate(
UserId,
TemplateId
);
if (ActiveRunState.Session.UserId == UserId)
{
RefreshRepositoryViews();
}
RefreshRepositoryViews();
return true;
}
@ -259,19 +260,18 @@ bool UHyperTwistTrainingSubsystem::SaveTrainingTimerExportPacketToDefaultPath(
bool UHyperTwistTrainingSubsystem::ImportTrainingTimerExportPacket(const FHyperTwistTrainingTimerExportPacket& TimerExportPacket)
{
if (!TimerExportPacket.IsStructurallyValid())
const FHyperTwistTrainingTimerExportPacket NormalizedPacket =
UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(TimerExportPacket);
if (!NormalizedPacket.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::ImportTimerResults(
TrainingRepositoryState,
TimerExportPacket
NormalizedPacket
);
if (ActiveRunState.Session.IsStructurallyValid())
{
RefreshRepositoryViews();
}
RefreshRepositoryViews();
return true;
}
@ -590,22 +590,9 @@ void UHyperTwistTrainingSubsystem::ClearActiveRun()
{
ActiveRunState = FHyperTwistTrainingRunState();
ActiveRunSummary = FHyperTwistTrainingSessionSummary();
ActiveDeckHistorySummary = FHyperTwistTrainingDeckHistorySummary();
ActiveTimingTrendSummary = FHyperTwistTrainingTimingTrendSummary();
ActivePersonalBestSummary = FHyperTwistTrainingPersonalBestSummary();
ActiveDeckScopedStats = FHyperTwistTrainingDeckScopedStats();
ActiveCurrentCaseScopedStats = FHyperTwistTrainingCaseScopedStats();
ActiveLearnerDeckStateSummary = FHyperTwistTrainingLearnerDeckStateSummary();
ActiveLearnerProfile = FHyperTwistTrainingLearnerProfile();
ActiveCoachMemorySnapshot = FHyperTwistTrainingCoachMemorySnapshot();
ActiveLearnerMethodSegments.Reset();
ActiveLearnerPresets.Reset();
ActiveTrainingSessionTemplates.Reset();
ActiveCaseRecommendations.Reset();
ActiveReviewPlanState = FHyperTwistTrainingReviewPlanState();
ActiveReviewFlowStatus = FHyperTwistTrainingReviewFlowStatus();
ActiveReviewProgramSummary = FHyperTwistTrainingReviewProgramSummary();
bHasActiveRun = false;
RefreshRepositoryViews();
}
void UHyperTwistTrainingSubsystem::RefreshSummary()
@ -624,6 +611,11 @@ void UHyperTwistTrainingSubsystem::RefreshSummary()
void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
{
ActiveRepositoryIntegrityReport = UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(
TrainingRepositoryState
);
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(TrainingRepositoryState);
if (!ActiveRunState.Session.IsStructurallyValid())
{
ActiveDeckHistorySummary = FHyperTwistTrainingDeckHistorySummary();

View file

@ -80,6 +80,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingRepositoryState MakeSampleTrainingRepositoryState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingRepositoryIntegrityReport MakeSampleTrainingRepositoryIntegrityReport();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingDeckHistorySummary MakeSampleTrainingDeckHistorySummary();
@ -92,6 +95,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingTimerExportPacket MakeSampleTrainingTimerExportPacket();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingTimerExportIntegrityReport MakeSampleTrainingTimerExportIntegrityReport();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingCaseScopedStats MakeSampleTrainingCaseScopedStats();

View file

@ -127,6 +127,26 @@ public:
const FHyperTwistTrainingTimerExportPacket& TimerExportPacket
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
static FHyperTwistTrainingTimerExportIntegrityReport DeriveTimerExportIntegrityReport(
const FHyperTwistTrainingTimerExportPacket& TimerExportPacket
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
static FHyperTwistTrainingTimerExportPacket NormalizeTimerExportPacket(
const FHyperTwistTrainingTimerExportPacket& TimerExportPacket
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
static FHyperTwistTrainingRepositoryIntegrityReport DeriveRepositoryIntegrityReport(
const FHyperTwistTrainingRepositoryState& RepositoryState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
static FHyperTwistTrainingRepositoryState RepairRepositoryState(
const FHyperTwistTrainingRepositoryState& RepositoryState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
static FHyperTwistTrainingCaseScopedStats DeriveCaseScopedStats(
const FHyperTwistTrainingRepositoryState& RepositoryState,

View file

@ -28,6 +28,11 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingRepositoryState GetTrainingRepositoryState(UObject* WorldContextObject);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingRepositoryIntegrityReport GetActiveTrainingRepositoryIntegrityReport(
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingDeckHistorySummary GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject);

View file

@ -23,6 +23,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
FHyperTwistTrainingRepositoryState GetTrainingRepositoryState() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
FHyperTwistTrainingRepositoryIntegrityReport GetActiveRepositoryIntegrityReport() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
FHyperTwistTrainingDeckHistorySummary GetActiveDeckHistorySummary() const;
@ -216,6 +219,9 @@ private:
UPROPERTY()
FHyperTwistTrainingRepositoryState TrainingRepositoryState;
UPROPERTY()
FHyperTwistTrainingRepositoryIntegrityReport ActiveRepositoryIntegrityReport;
UPROPERTY()
FHyperTwistTrainingDeckHistorySummary ActiveDeckHistorySummary;

View file

@ -1287,6 +1287,44 @@ struct FHyperTwistTrainingTimerExportPacket
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingTimerExportIntegrityReport
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 MismatchedScopeEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 MissingPacketMetadataCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> RepairActions;
bool HasIntegrityIssues() const
{
return InvalidEntryCount > 0
|| DuplicateEntryCount > 0
|| MismatchedScopeEntryCount > 0
|| MissingPacketMetadataCount > 0;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCaseScopedStats
{
@ -2242,3 +2280,60 @@ struct FHyperTwistTrainingRepositoryState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingSessionTemplate> StoredSessionTemplates;
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingRepositoryIntegrityReport
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalRunRecordCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidRunRecordCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateRunRecordCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalLearnerStateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidLearnerStateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateLearnerStateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalReviewPlanCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidReviewPlanCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateReviewPlanCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalStoredSessionTemplateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidStoredSessionTemplateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateStoredSessionTemplateCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> RepairActions;
bool HasIntegrityIssues() const
{
return InvalidRunRecordCount > 0
|| DuplicateRunRecordCount > 0
|| InvalidLearnerStateCount > 0
|| DuplicateLearnerStateCount > 0
|| InvalidReviewPlanCount > 0
|| DuplicateReviewPlanCount > 0
|| InvalidStoredSessionTemplateCount > 0
|| DuplicateStoredSessionTemplateCount > 0;
}
};