diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index ae7d5f0..ef0fbe7 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -300,6 +300,83 @@ namespace HyperTwistContractLibraryInternal return Normalized; } + const TArray& GetClassicCorrectionFaceOrder() + { + static const TArray FaceOrder = { + TEXT("U"), + TEXT("R"), + TEXT("F"), + TEXT("D"), + TEXT("L"), + TEXT("B") + }; + return FaceOrder; + } + + int32 GetClassicCorrectionFaceOrderIndex(const FString& FaceId) + { + const FString NormalizedFaceId = NormalizeBrowserFaceHint(FaceId); + const TArray& FaceOrder = GetClassicCorrectionFaceOrder(); + for (int32 Index = 0; Index < FaceOrder.Num(); ++Index) + { + if (FaceOrder[Index] == NormalizedFaceId) + { + return Index; + } + } + + return MAX_int32; + } + + float CalculateCommittedFaceConfidence(const FHyperTwistVisionCommittedFaceState& FaceState) + { + if (FaceState.LatestOrderedStickers.Num() <= 0) + { + return 0.0f; + } + + float ConfidenceSum = 0.0f; + for (const FHyperTwistVisionObservedSticker& Sticker : FaceState.LatestOrderedStickers) + { + ConfidenceSum += Sticker.Confidence; + } + + return ConfidenceSum / static_cast(FaceState.LatestOrderedStickers.Num()); + } + + void UpsertCorrectionTarget( + TArray& Targets, + const FHyperTwistVisionCorrectionTarget& Candidate + ) + { + const FString CandidateFaceId = NormalizeBrowserFaceHint(Candidate.FaceId); + const int32 ExistingIndex = Targets.IndexOfByPredicate( + [&CandidateFaceId](const FHyperTwistVisionCorrectionTarget& ExistingTarget) + { + return NormalizeBrowserFaceHint(ExistingTarget.FaceId) == CandidateFaceId; + } + ); + + if (ExistingIndex == INDEX_NONE) + { + Targets.Add(Candidate); + return; + } + + if (Candidate.PriorityOrdinal < Targets[ExistingIndex].PriorityOrdinal) + { + Targets[ExistingIndex] = Candidate; + return; + } + + if (Candidate.PriorityOrdinal == Targets[ExistingIndex].PriorityOrdinal + && Candidate.bManualReviewPreferred + && !Targets[ExistingIndex].bManualReviewPreferred) + { + Targets[ExistingIndex].bManualReviewPreferred = true; + } + } + FHyperTwistVisionBrowserShellProfile MakeDefaultClassicCubeBrowserShellProfile(const FString& ProfileId) { FHyperTwistVisionBrowserShellProfile Profile; @@ -452,6 +529,36 @@ namespace HyperTwistContractLibraryInternal return Profile; } + FHyperTwistVisionCorrectionProfile MakeDefaultClassicCubeCorrectionProfile(const FString& ProfileId) + { + FHyperTwistVisionCorrectionProfile Profile; + Profile.CorrectionProfileId = ProfileId; + Profile.PuzzleId = TEXT("cube/3x3x3"); + Profile.CorrectionMode = TEXT("bounded-multi-face-correction-v1"); + Profile.bSupportsTargetedRescan = true; + Profile.bSupportsManualCubeEdit = true; + Profile.bSupportsStageRestart = true; + + auto AddAction = [&Profile]( + const TCHAR* ActionId, + const TCHAR* InputBinding, + const TCHAR* SurfaceId + ) + { + FHyperTwistVisionShellActionBinding ActionBinding; + ActionBinding.ActionId = ActionId; + ActionBinding.InputBinding = InputBinding; + ActionBinding.SurfaceId = SurfaceId; + Profile.ActionBindings.Add(ActionBinding); + }; + + AddAction(TEXT("capture-target-face"), TEXT("button:capture-target-face"), TEXT("video-stream")); + AddAction(TEXT("rescan-target-face"), TEXT("button:rescan-target-face"), TEXT("video-stream")); + AddAction(TEXT("apply-manual-corrections"), TEXT("button:apply-corrections"), TEXT("cube-edit")); + AddAction(TEXT("restart-browser-session"), TEXT("button:restart-session"), TEXT("cube-edit")); + return Profile; + } + void ResolveMockClassicFaceSwatch( const FString& FaceId, FString& OutColorId, @@ -718,9 +825,275 @@ namespace HyperTwistContractLibraryInternal return BrowserShellState; } + FHyperTwistVisionCorrectionState MakeClassicCubeCorrectionState( + const FHyperTwistVisionSessionConfig& SessionConfig, + const FHyperTwistVisionReconstructionSession& ReconstructionSession, + const FHyperTwistVisionFaceObservation& ObservedFace, + const TArray& Warnings + ) + { + static constexpr float LowConfidenceThreshold = 0.85f; + + FHyperTwistVisionCorrectionState CorrectionState; + const FHyperTwistVisionCorrectionProfile CorrectionProfile = + SessionConfig.CorrectionProfileDefinition.IsStructurallyValid() + ? SessionConfig.CorrectionProfileDefinition + : MakeDefaultClassicCubeCorrectionProfile( + !SessionConfig.CorrectionProfileId.IsEmpty() + ? SessionConfig.CorrectionProfileId + : TEXT("classic-cube-correction-shell-v1") + ); + + CorrectionState.CorrectionProfileId = CorrectionProfile.CorrectionProfileId; + CorrectionState.MissingFaceCount = ReconstructionSession.IsStructurallyValid() + ? ReconstructionSession.MissingFaces.Num() + : 6; + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit && ReconstructionSession.bHasCompleteClassicCubeNet; + CorrectionState.bStageRestartVisible = + CorrectionProfile.bSupportsStageRestart && Warnings.Num() > 0; + + TArray PendingTargets; + TArray Contradictions; + if (ReconstructionSession.IsStructurallyValid()) + { + for (const FHyperTwistVisionCommittedFaceState& FaceState : ReconstructionSession.CommittedFaces) + { + const FString FaceId = NormalizeBrowserFaceHint(FaceState.FaceId); + const bool bHasCompleteNet = ReconstructionSession.bHasCompleteClassicCubeNet; + + if (FaceState.bHasConflicts) + { + FHyperTwistVisionCorrectionContradiction Contradiction; + Contradiction.ContradictionId = FString::Printf(TEXT("conflict-%s"), *FaceId); + Contradiction.FaceId = FaceId; + Contradiction.ContradictionKind = TEXT("conflicting-face"); + Contradiction.ObservationId = FaceState.LatestObservationId; + Contradiction.RevisionCount = FaceState.RevisionCount; + Contradiction.StatusLine = + FString::Printf(TEXT("Face %s has conflicting sticker votes."), *FaceId); + Contradiction.DetailLine = + TEXT("At least one sticker vote disagrees with the current winner. Re-scan the face or review the manual cube edit before solve guidance continues."); + Contradiction.bBlocksSolveExplanation = true; + Contradiction.bManualReviewPreferred = bHasCompleteNet; + for (const FHyperTwistVisionReconstructionStickerVote& Vote : FaceState.StickerVotes) + { + if (Vote.bHadConflict) + { + Contradiction.AffectedStickerIndices.Add(Vote.GridIndex); + } + } + Contradictions.Add(Contradiction); + + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("conflicting-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan face %s before capture advances."), *FaceId); + Target.DetailLine = bHasCompleteNet + ? TEXT("Conflicting sticker votes survived the bounded reconstruction pass. Use cube edit or a targeted re-scan to close the contradiction.") + : TEXT("Conflicting sticker votes are present while capture is still open. Re-scan the face before committing the remaining sides."); + Target.PriorityOrdinal = 0; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + + if (FaceState.RevisionCount > 0) + { + FHyperTwistVisionCorrectionContradiction Contradiction; + Contradiction.ContradictionId = FString::Printf(TEXT("revision-%s"), *FaceId); + Contradiction.FaceId = FaceId; + Contradiction.ContradictionKind = TEXT("manual-revision"); + Contradiction.ObservationId = FaceState.LatestObservationId; + Contradiction.RevisionCount = FaceState.RevisionCount; + Contradiction.StatusLine = + FString::Printf(TEXT("Face %s was recommitted and needs review."), *FaceId); + Contradiction.DetailLine = FString::Printf( + TEXT("The latest bounded reconstruction includes %d revision(s) on face %s. Preserve the latest capture or review the cube edit before solve guidance continues."), + FaceState.RevisionCount, + *FaceId + ); + Contradiction.bBlocksSolveExplanation = true; + Contradiction.bManualReviewPreferred = bHasCompleteNet; + Contradictions.Add(Contradiction); + + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("revised-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review the revised face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan revised face %s before capture advances."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Face %s has already been recommitted %d time(s) in this bounded session."), + *FaceId, + FaceState.RevisionCount + ); + Target.PriorityOrdinal = 1; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + + const float FaceConfidence = CalculateCommittedFaceConfidence(FaceState); + if (FaceConfidence > 0.0f && FaceConfidence < LowConfidenceThreshold) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("low-confidence-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review low-confidence face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan low-confidence face %s before capture advances."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Average sticker confidence %.2f on face %s is below the bounded correction threshold %.2f."), + FaceConfidence, + *FaceId, + LowConfidenceThreshold + ); + Target.PriorityOrdinal = 2; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + for (const FString& MissingFaceId : ReconstructionSession.MissingFaces) + { + const FString FaceId = NormalizeBrowserFaceHint(MissingFaceId); + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("missing-face"); + Target.RecommendedActionId = TEXT("capture-target-face"); + Target.StatusLine = + FString::Printf(TEXT("Capture face %s to continue the bounded reconstruction pass."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Commit the next missing face. %d face(s) still need capture before solve guidance unlocks."), + ReconstructionSession.MissingFaces.Num() + ); + Target.PriorityOrdinal = 3; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + if (PendingTargets.Num() == 0) + { + const FString FallbackFaceId = !ObservedFace.FaceId.IsEmpty() + ? NormalizeBrowserFaceHint(ObservedFace.FaceId) + : TEXT("F"); + + if (Warnings.Num() > 0) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FallbackFaceId; + Target.ReasonId = TEXT("warning-restart"); + Target.RecommendedActionId = TEXT("restart-browser-session"); + Target.StatusLine = TEXT("Restart the capture stage before correction continues."); + Target.DetailLine = FString::Printf( + TEXT("The bounded correction shell reported: %s"), + *FString::Join(Warnings, TEXT(" | ")) + ); + Target.PriorityOrdinal = 0; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + else if (!ReconstructionSession.IsStructurallyValid()) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FallbackFaceId; + Target.ReasonId = TEXT("capture-bootstrap"); + Target.RecommendedActionId = TEXT("capture-target-face"); + Target.StatusLine = + FString::Printf(TEXT("Capture face %s to start bounded correction routing."), *FallbackFaceId); + Target.DetailLine = + TEXT("No committed reconstruction session is available yet. Capture the first face before correction or solve guidance can advance."); + Target.PriorityOrdinal = 3; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + PendingTargets.Sort( + [](const FHyperTwistVisionCorrectionTarget& Left, const FHyperTwistVisionCorrectionTarget& Right) + { + if (Left.PriorityOrdinal != Right.PriorityOrdinal) + { + return Left.PriorityOrdinal < Right.PriorityOrdinal; + } + + return GetClassicCorrectionFaceOrderIndex(Left.FaceId) + < GetClassicCorrectionFaceOrderIndex(Right.FaceId); + } + ); + Contradictions.Sort( + [](const FHyperTwistVisionCorrectionContradiction& Left, const FHyperTwistVisionCorrectionContradiction& Right) + { + return GetClassicCorrectionFaceOrderIndex(Left.FaceId) + < GetClassicCorrectionFaceOrderIndex(Right.FaceId); + } + ); + + CorrectionState.PendingTargets = PendingTargets; + CorrectionState.Contradictions = Contradictions; + CorrectionState.ContradictionCount = Contradictions.Num(); + CorrectionState.bCorrectionRequired = PendingTargets.Num() > 0; + CorrectionState.bManualReviewRequired = Contradictions.Num() > 0; + CorrectionState.bCorrectionComplete = + ReconstructionSession.bHasCompleteClassicCubeNet && !CorrectionState.bCorrectionRequired; + CorrectionState.bSolveExplanationBlocked = !CorrectionState.bCorrectionComplete; + + if (CorrectionState.bCorrectionRequired) + { + CorrectionState.ActiveTarget = PendingTargets[0]; + CorrectionState.ActiveTargetFaceId = CorrectionState.ActiveTarget.FaceId; + CorrectionState.RecommendationStatusLine = CorrectionState.ActiveTarget.StatusLine; + CorrectionState.RecommendationDetailLine = CorrectionState.ActiveTarget.DetailLine; + CorrectionState.NextRecommendedActionId = CorrectionState.ActiveTarget.RecommendedActionId; + CorrectionState.bTargetedRescanVisible = + CorrectionState.ActiveTarget.RecommendedActionId == TEXT("capture-target-face") + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("rescan-target-face"); + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit + && (ReconstructionSession.bHasCompleteClassicCubeNet + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("apply-manual-corrections")); + CorrectionState.bStageRestartVisible = + CorrectionProfile.bSupportsStageRestart + && (Warnings.Num() > 0 + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("restart-browser-session")); + CorrectionState.bManualReviewRequired = + CorrectionState.bManualReviewRequired || CorrectionState.ActiveTarget.bManualReviewPreferred; + CorrectionState.RecommendedStageId = + CorrectionState.ActiveTarget.RecommendedActionId == TEXT("apply-manual-corrections") + ? TEXT("cube-edit") + : TEXT("video-stream"); + } + else + { + CorrectionState.RecommendedStageId = TEXT("orient-centers"); + CorrectionState.RecommendationStatusLine = + TEXT("Correction is clear; solve guidance can begin."); + CorrectionState.RecommendationDetailLine = + TEXT("No missing faces, contradictions, or low-confidence targets remain in the bounded correction shell."); + CorrectionState.NextRecommendedActionId = TEXT("start-solve-explanation"); + CorrectionState.bTargetedRescanVisible = false; + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit && ReconstructionSession.bHasCompleteClassicCubeNet; + } + + return CorrectionState; + } + FHyperTwistVisionSolveExplanationState MakeClassicCubeSolveExplanationState( const FHyperTwistVisionSessionConfig& SessionConfig, - const FHyperTwistVisionReconstructionSession& ReconstructionSession + const FHyperTwistVisionReconstructionSession& ReconstructionSession, + const FHyperTwistVisionCorrectionState& CorrectionState ) { FHyperTwistVisionSolveExplanationState SolveExplanationState; @@ -738,10 +1111,9 @@ namespace HyperTwistContractLibraryInternal SolveExplanationState.bPlaybackDeferred = !SolveExplanationProfile.bSupportsPlaybackStage; SolveExplanationState.bRequiresFullReconstruction = true; - const int32 RemainingFaceCount = ReconstructionSession.IsStructurallyValid() - ? ReconstructionSession.MissingFaces.Num() - : 6; - if (ReconstructionSession.bHasCompleteClassicCubeNet) + if (ReconstructionSession.bHasCompleteClassicCubeNet + && CorrectionState.IsStructurallyValid() + && CorrectionState.bCorrectionComplete) { SolveExplanationState.RecommendedStageId = SolveExplanationProfile.Steps.Num() > 0 @@ -757,8 +1129,30 @@ namespace HyperTwistContractLibraryInternal SolveExplanationState.bNextStepVisible = false; SolveExplanationState.bPreviousStepVisible = false; } + else if (ReconstructionSession.bHasCompleteClassicCubeNet + && CorrectionState.IsStructurallyValid() + && CorrectionState.bCorrectionRequired) + { + SolveExplanationState.RecommendedStageId = !CorrectionState.RecommendedStageId.IsEmpty() + ? CorrectionState.RecommendedStageId + : TEXT("video-stream"); + SolveExplanationState.RecommendationStatusLine = + TEXT("Solve guidance stays blocked until correction closes."); + SolveExplanationState.RecommendationDetailLine = + !CorrectionState.RecommendationDetailLine.IsEmpty() + ? CorrectionState.RecommendationDetailLine + : TEXT("Finish the bounded correction shell before solve guidance unlocks."); + SolveExplanationState.CurrentStepOrdinal = -1; + SolveExplanationState.bReadyForExplanation = false; + SolveExplanationState.bStartExplanationVisible = false; + SolveExplanationState.bNextStepVisible = false; + SolveExplanationState.bPreviousStepVisible = false; + } else { + const int32 RemainingFaceCount = ReconstructionSession.IsStructurallyValid() + ? ReconstructionSession.MissingFaces.Num() + : 6; SolveExplanationState.RecommendedStageId = TEXT("capture-remaining-faces"); SolveExplanationState.RecommendationStatusLine = TEXT("Solve guidance unlocks after capture completes."); @@ -1019,6 +1413,11 @@ FHyperTwistVisionSessionConfig UHyperTwistContractLibrary::MakeSampleVisionSessi HyperTwistContractLibraryInternal::MakeDefaultClassicCubeSolveExplanationProfile( Config.SolveExplanationProfileId ); + Config.CorrectionProfileId = TEXT("classic-cube-correction-shell-v1"); + Config.CorrectionProfileDefinition = + HyperTwistContractLibraryInternal::MakeDefaultClassicCubeCorrectionProfile( + Config.CorrectionProfileId + ); Config.FrameSize = FIntPoint(1280, 720); Config.MaxFrameRate = 30; return Config; @@ -1091,9 +1490,16 @@ FHyperTwistVisionCommitResult UHyperTwistContractLibrary::MakeMockVisionCommitRe Result.ReconstructionSession, Result.ObservedFace ); + Result.CorrectionState = HyperTwistContractLibraryInternal::MakeClassicCubeCorrectionState( + SessionConfig, + Result.ReconstructionSession, + Result.ObservedFace, + Result.Warnings + ); Result.SolveExplanationState = HyperTwistContractLibraryInternal::MakeClassicCubeSolveExplanationState( SessionConfig, - Result.ReconstructionSession + Result.ReconstructionSession, + Result.CorrectionState ); return Result; } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 5bd57cb..89f337b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -1928,6 +1928,55 @@ namespace HyperTwistTrainingSubsystemInternal return MAX_int32; } + float CalculateCommittedFaceConfidence(const FHyperTwistVisionCommittedFaceState& FaceState) + { + if (FaceState.LatestOrderedStickers.Num() <= 0) + { + return 0.0f; + } + + float ConfidenceSum = 0.0f; + for (const FHyperTwistVisionObservedSticker& Sticker : FaceState.LatestOrderedStickers) + { + ConfidenceSum += Sticker.Confidence; + } + + return ConfidenceSum / static_cast(FaceState.LatestOrderedStickers.Num()); + } + + void UpsertCorrectionTarget( + TArray& Targets, + const FHyperTwistVisionCorrectionTarget& Candidate + ) + { + const FString CandidateFaceId = NormalizeRecognitionFaceId(Candidate.FaceId); + const int32 ExistingIndex = Targets.IndexOfByPredicate( + [&CandidateFaceId](const FHyperTwistVisionCorrectionTarget& ExistingTarget) + { + return NormalizeRecognitionFaceId(ExistingTarget.FaceId) == CandidateFaceId; + } + ); + + if (ExistingIndex == INDEX_NONE) + { + Targets.Add(Candidate); + return; + } + + if (Candidate.PriorityOrdinal < Targets[ExistingIndex].PriorityOrdinal) + { + Targets[ExistingIndex] = Candidate; + return; + } + + if (Candidate.PriorityOrdinal == Targets[ExistingIndex].PriorityOrdinal + && Candidate.bManualReviewPreferred + && !Targets[ExistingIndex].bManualReviewPreferred) + { + Targets[ExistingIndex].bManualReviewPreferred = true; + } + } + const FHyperTwistVisionCommittedFaceState* FindCommittedFaceState( const FHyperTwistVisionReconstructionSession& Session, const FString& FaceId @@ -2588,6 +2637,38 @@ namespace HyperTwistTrainingSubsystemInternal return Profile; } + FHyperTwistVisionCorrectionProfile BuildDefaultClassicCubeCorrectionProfile( + const FString& ProfileId + ) + { + FHyperTwistVisionCorrectionProfile Profile; + Profile.CorrectionProfileId = ProfileId; + Profile.PuzzleId = TEXT("cube/3x3x3"); + Profile.CorrectionMode = TEXT("bounded-multi-face-correction-v1"); + Profile.bSupportsTargetedRescan = true; + Profile.bSupportsManualCubeEdit = true; + Profile.bSupportsStageRestart = true; + + auto AddAction = [&Profile]( + const TCHAR* ActionId, + const TCHAR* InputBinding, + const TCHAR* SurfaceId + ) + { + FHyperTwistVisionShellActionBinding ActionBinding; + ActionBinding.ActionId = ActionId; + ActionBinding.InputBinding = InputBinding; + ActionBinding.SurfaceId = SurfaceId; + Profile.ActionBindings.Add(ActionBinding); + }; + + AddAction(TEXT("capture-target-face"), TEXT("button:capture-target-face"), TEXT("video-stream")); + AddAction(TEXT("rescan-target-face"), TEXT("button:rescan-target-face"), TEXT("video-stream")); + AddAction(TEXT("apply-manual-corrections"), TEXT("button:apply-corrections"), TEXT("cube-edit")); + AddAction(TEXT("restart-browser-session"), TEXT("button:restart-session"), TEXT("cube-edit")); + return Profile; + } + FHyperTwistVisionShellState BuildClassicCubeWebcamShellState( const FHyperTwistVisionSessionConfig& SessionConfig, const FHyperTwistVisionReconstructionSession& ReconstructionSession, @@ -2695,9 +2776,275 @@ namespace HyperTwistTrainingSubsystemInternal return BrowserShellState; } + FHyperTwistVisionCorrectionState BuildClassicCubeCorrectionState( + const FHyperTwistVisionSessionConfig& SessionConfig, + const FHyperTwistVisionReconstructionSession& ReconstructionSession, + const FHyperTwistVisionFaceObservation& ObservedFace, + const TArray& Warnings + ) + { + static constexpr float LowConfidenceThreshold = 0.85f; + + FHyperTwistVisionCorrectionState CorrectionState; + const FHyperTwistVisionCorrectionProfile CorrectionProfile = + SessionConfig.CorrectionProfileDefinition.IsStructurallyValid() + ? SessionConfig.CorrectionProfileDefinition + : BuildDefaultClassicCubeCorrectionProfile( + !SessionConfig.CorrectionProfileId.IsEmpty() + ? SessionConfig.CorrectionProfileId + : TEXT("classic-cube-correction-shell-v1") + ); + + CorrectionState.CorrectionProfileId = CorrectionProfile.CorrectionProfileId; + CorrectionState.MissingFaceCount = ReconstructionSession.IsStructurallyValid() + ? ReconstructionSession.MissingFaces.Num() + : 6; + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit && ReconstructionSession.bHasCompleteClassicCubeNet; + CorrectionState.bStageRestartVisible = + CorrectionProfile.bSupportsStageRestart && Warnings.Num() > 0; + + TArray PendingTargets; + TArray Contradictions; + if (ReconstructionSession.IsStructurallyValid()) + { + for (const FHyperTwistVisionCommittedFaceState& FaceState : ReconstructionSession.CommittedFaces) + { + const FString FaceId = NormalizeRecognitionFaceId(FaceState.FaceId); + const bool bHasCompleteNet = ReconstructionSession.bHasCompleteClassicCubeNet; + + if (FaceState.bHasConflicts) + { + FHyperTwistVisionCorrectionContradiction Contradiction; + Contradiction.ContradictionId = FString::Printf(TEXT("conflict-%s"), *FaceId); + Contradiction.FaceId = FaceId; + Contradiction.ContradictionKind = TEXT("conflicting-face"); + Contradiction.ObservationId = FaceState.LatestObservationId; + Contradiction.RevisionCount = FaceState.RevisionCount; + Contradiction.StatusLine = + FString::Printf(TEXT("Face %s has conflicting sticker votes."), *FaceId); + Contradiction.DetailLine = + TEXT("At least one sticker vote disagrees with the current winner. Re-scan the face or review the manual cube edit before solve guidance continues."); + Contradiction.bBlocksSolveExplanation = true; + Contradiction.bManualReviewPreferred = bHasCompleteNet; + for (const FHyperTwistVisionReconstructionStickerVote& Vote : FaceState.StickerVotes) + { + if (Vote.bHadConflict) + { + Contradiction.AffectedStickerIndices.Add(Vote.GridIndex); + } + } + Contradictions.Add(Contradiction); + + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("conflicting-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan face %s before capture advances."), *FaceId); + Target.DetailLine = bHasCompleteNet + ? TEXT("Conflicting sticker votes survived the bounded reconstruction pass. Use cube edit or a targeted re-scan to close the contradiction.") + : TEXT("Conflicting sticker votes are present while capture is still open. Re-scan the face before committing the remaining sides."); + Target.PriorityOrdinal = 0; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + + if (FaceState.RevisionCount > 0) + { + FHyperTwistVisionCorrectionContradiction Contradiction; + Contradiction.ContradictionId = FString::Printf(TEXT("revision-%s"), *FaceId); + Contradiction.FaceId = FaceId; + Contradiction.ContradictionKind = TEXT("manual-revision"); + Contradiction.ObservationId = FaceState.LatestObservationId; + Contradiction.RevisionCount = FaceState.RevisionCount; + Contradiction.StatusLine = + FString::Printf(TEXT("Face %s was recommitted and needs review."), *FaceId); + Contradiction.DetailLine = FString::Printf( + TEXT("The latest bounded reconstruction includes %d revision(s) on face %s. Preserve the latest capture or review the cube edit before solve guidance continues."), + FaceState.RevisionCount, + *FaceId + ); + Contradiction.bBlocksSolveExplanation = true; + Contradiction.bManualReviewPreferred = bHasCompleteNet; + Contradictions.Add(Contradiction); + + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("revised-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review the revised face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan revised face %s before capture advances."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Face %s has already been recommitted %d time(s) in this bounded session."), + *FaceId, + FaceState.RevisionCount + ); + Target.PriorityOrdinal = 1; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + + const float FaceConfidence = CalculateCommittedFaceConfidence(FaceState); + if (FaceConfidence > 0.0f && FaceConfidence < LowConfidenceThreshold) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("low-confidence-face"); + Target.RecommendedActionId = bHasCompleteNet + ? TEXT("apply-manual-corrections") + : TEXT("rescan-target-face"); + Target.StatusLine = bHasCompleteNet + ? FString::Printf(TEXT("Review low-confidence face %s before solve guidance begins."), *FaceId) + : FString::Printf(TEXT("Re-scan low-confidence face %s before capture advances."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Average sticker confidence %.2f on face %s is below the bounded correction threshold %.2f."), + FaceConfidence, + *FaceId, + LowConfidenceThreshold + ); + Target.PriorityOrdinal = 2; + Target.bManualReviewPreferred = bHasCompleteNet; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + for (const FString& MissingFaceId : ReconstructionSession.MissingFaces) + { + const FString FaceId = NormalizeRecognitionFaceId(MissingFaceId); + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FaceId; + Target.ReasonId = TEXT("missing-face"); + Target.RecommendedActionId = TEXT("capture-target-face"); + Target.StatusLine = + FString::Printf(TEXT("Capture face %s to continue the bounded reconstruction pass."), *FaceId); + Target.DetailLine = FString::Printf( + TEXT("Commit the next missing face. %d face(s) still need capture before solve guidance unlocks."), + ReconstructionSession.MissingFaces.Num() + ); + Target.PriorityOrdinal = 3; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + if (PendingTargets.Num() == 0) + { + const FString FallbackFaceId = !ObservedFace.FaceId.IsEmpty() + ? NormalizeRecognitionFaceId(ObservedFace.FaceId) + : TEXT("F"); + + if (Warnings.Num() > 0) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FallbackFaceId; + Target.ReasonId = TEXT("warning-restart"); + Target.RecommendedActionId = TEXT("restart-browser-session"); + Target.StatusLine = TEXT("Restart the capture stage before correction continues."); + Target.DetailLine = FString::Printf( + TEXT("The bounded correction shell reported: %s"), + *FString::Join(Warnings, TEXT(" | ")) + ); + Target.PriorityOrdinal = 0; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + else if (!ReconstructionSession.IsStructurallyValid()) + { + FHyperTwistVisionCorrectionTarget Target; + Target.FaceId = FallbackFaceId; + Target.ReasonId = TEXT("capture-bootstrap"); + Target.RecommendedActionId = TEXT("capture-target-face"); + Target.StatusLine = + FString::Printf(TEXT("Capture face %s to start bounded correction routing."), *FallbackFaceId); + Target.DetailLine = + TEXT("No committed reconstruction session is available yet. Capture the first face before correction or solve guidance can advance."); + Target.PriorityOrdinal = 3; + Target.bManualReviewPreferred = false; + UpsertCorrectionTarget(PendingTargets, Target); + } + } + + PendingTargets.Sort( + [](const FHyperTwistVisionCorrectionTarget& Left, const FHyperTwistVisionCorrectionTarget& Right) + { + if (Left.PriorityOrdinal != Right.PriorityOrdinal) + { + return Left.PriorityOrdinal < Right.PriorityOrdinal; + } + + return GetClassicRecognitionFaceOrderIndex(Left.FaceId) + < GetClassicRecognitionFaceOrderIndex(Right.FaceId); + } + ); + Contradictions.Sort( + [](const FHyperTwistVisionCorrectionContradiction& Left, const FHyperTwistVisionCorrectionContradiction& Right) + { + return GetClassicRecognitionFaceOrderIndex(Left.FaceId) + < GetClassicRecognitionFaceOrderIndex(Right.FaceId); + } + ); + + CorrectionState.PendingTargets = PendingTargets; + CorrectionState.Contradictions = Contradictions; + CorrectionState.ContradictionCount = Contradictions.Num(); + CorrectionState.bCorrectionRequired = PendingTargets.Num() > 0; + CorrectionState.bManualReviewRequired = Contradictions.Num() > 0; + CorrectionState.bCorrectionComplete = + ReconstructionSession.bHasCompleteClassicCubeNet && !CorrectionState.bCorrectionRequired; + CorrectionState.bSolveExplanationBlocked = !CorrectionState.bCorrectionComplete; + + if (CorrectionState.bCorrectionRequired) + { + CorrectionState.ActiveTarget = PendingTargets[0]; + CorrectionState.ActiveTargetFaceId = CorrectionState.ActiveTarget.FaceId; + CorrectionState.RecommendationStatusLine = CorrectionState.ActiveTarget.StatusLine; + CorrectionState.RecommendationDetailLine = CorrectionState.ActiveTarget.DetailLine; + CorrectionState.NextRecommendedActionId = CorrectionState.ActiveTarget.RecommendedActionId; + CorrectionState.bTargetedRescanVisible = + CorrectionState.ActiveTarget.RecommendedActionId == TEXT("capture-target-face") + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("rescan-target-face"); + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit + && (ReconstructionSession.bHasCompleteClassicCubeNet + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("apply-manual-corrections")); + CorrectionState.bStageRestartVisible = + CorrectionProfile.bSupportsStageRestart + && (Warnings.Num() > 0 + || CorrectionState.ActiveTarget.RecommendedActionId == TEXT("restart-browser-session")); + CorrectionState.bManualReviewRequired = + CorrectionState.bManualReviewRequired || CorrectionState.ActiveTarget.bManualReviewPreferred; + CorrectionState.RecommendedStageId = + CorrectionState.ActiveTarget.RecommendedActionId == TEXT("apply-manual-corrections") + ? TEXT("cube-edit") + : TEXT("video-stream"); + } + else + { + CorrectionState.RecommendedStageId = TEXT("orient-centers"); + CorrectionState.RecommendationStatusLine = + TEXT("Correction is clear; solve guidance can begin."); + CorrectionState.RecommendationDetailLine = + TEXT("No missing faces, contradictions, or low-confidence targets remain in the bounded correction shell."); + CorrectionState.NextRecommendedActionId = TEXT("start-solve-explanation"); + CorrectionState.bTargetedRescanVisible = false; + CorrectionState.bManualCubeEditVisible = + CorrectionProfile.bSupportsManualCubeEdit && ReconstructionSession.bHasCompleteClassicCubeNet; + } + + return CorrectionState; + } + FHyperTwistVisionSolveExplanationState BuildClassicCubeSolveExplanationState( const FHyperTwistVisionSessionConfig& SessionConfig, - const FHyperTwistVisionReconstructionSession& ReconstructionSession + const FHyperTwistVisionReconstructionSession& ReconstructionSession, + const FHyperTwistVisionCorrectionState& CorrectionState ) { FHyperTwistVisionSolveExplanationState SolveExplanationState; @@ -2715,10 +3062,9 @@ namespace HyperTwistTrainingSubsystemInternal SolveExplanationState.bPlaybackDeferred = !SolveExplanationProfile.bSupportsPlaybackStage; SolveExplanationState.bRequiresFullReconstruction = true; - const int32 RemainingFaceCount = ReconstructionSession.IsStructurallyValid() - ? ReconstructionSession.MissingFaces.Num() - : 6; - if (ReconstructionSession.bHasCompleteClassicCubeNet) + if (ReconstructionSession.bHasCompleteClassicCubeNet + && CorrectionState.IsStructurallyValid() + && CorrectionState.bCorrectionComplete) { SolveExplanationState.RecommendedStageId = SolveExplanationProfile.Steps.Num() > 0 @@ -2734,8 +3080,30 @@ namespace HyperTwistTrainingSubsystemInternal SolveExplanationState.bNextStepVisible = false; SolveExplanationState.bPreviousStepVisible = false; } + else if (ReconstructionSession.bHasCompleteClassicCubeNet + && CorrectionState.IsStructurallyValid() + && CorrectionState.bCorrectionRequired) + { + SolveExplanationState.RecommendedStageId = !CorrectionState.RecommendedStageId.IsEmpty() + ? CorrectionState.RecommendedStageId + : TEXT("video-stream"); + SolveExplanationState.RecommendationStatusLine = + TEXT("Solve guidance stays blocked until correction closes."); + SolveExplanationState.RecommendationDetailLine = + !CorrectionState.RecommendationDetailLine.IsEmpty() + ? CorrectionState.RecommendationDetailLine + : TEXT("Finish the bounded correction shell before solve guidance unlocks."); + SolveExplanationState.CurrentStepOrdinal = -1; + SolveExplanationState.bReadyForExplanation = false; + SolveExplanationState.bStartExplanationVisible = false; + SolveExplanationState.bNextStepVisible = false; + SolveExplanationState.bPreviousStepVisible = false; + } else { + const int32 RemainingFaceCount = ReconstructionSession.IsStructurallyValid() + ? ReconstructionSession.MissingFaces.Num() + : 6; SolveExplanationState.RecommendedStageId = TEXT("capture-remaining-faces"); SolveExplanationState.RecommendationStatusLine = TEXT("Solve guidance unlocks after capture completes."); @@ -7187,10 +7555,18 @@ FHyperTwistVisionCommitResult UHyperTwistTrainingSubsystem::CommitActiveRecognit Result.ReconstructionSession, Result.ObservedFace ); + Result.CorrectionState = + HyperTwistTrainingSubsystemInternal::BuildClassicCubeCorrectionState( + ActiveRecognitionSessionState.SessionConfig, + Result.ReconstructionSession, + Result.ObservedFace, + Result.Warnings + ); Result.SolveExplanationState = HyperTwistTrainingSubsystemInternal::BuildClassicCubeSolveExplanationState( ActiveRecognitionSessionState.SessionConfig, - Result.ReconstructionSession + Result.ReconstructionSession, + Result.CorrectionState ); } @@ -8186,6 +8562,28 @@ FHyperTwistVisionSessionConfig UHyperTwistTrainingSubsystem::BuildActiveRecognit SessionConfig.SolveExplanationProfileDefinition.ExplanationProfileId = SessionConfig.SolveExplanationProfileId; } + if (SessionConfig.CorrectionProfileId.IsEmpty() + && SessionConfig.CorrectionProfileDefinition.IsStructurallyValid()) + { + SessionConfig.CorrectionProfileId = + SessionConfig.CorrectionProfileDefinition.CorrectionProfileId; + } + if (SessionConfig.CorrectionProfileId.IsEmpty()) + { + SessionConfig.CorrectionProfileId = TEXT("classic-cube-correction-shell-v1"); + } + if (!SessionConfig.CorrectionProfileDefinition.IsStructurallyValid()) + { + SessionConfig.CorrectionProfileDefinition = + HyperTwistTrainingSubsystemInternal::BuildDefaultClassicCubeCorrectionProfile( + SessionConfig.CorrectionProfileId + ); + } + if (SessionConfig.CorrectionProfileDefinition.CorrectionProfileId.IsEmpty()) + { + SessionConfig.CorrectionProfileDefinition.CorrectionProfileId = + SessionConfig.CorrectionProfileId; + } } return SessionConfig; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h index ec44738..91e899f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h @@ -807,6 +807,232 @@ struct FHyperTwistVisionSolveExplanationState } }; +USTRUCT(BlueprintType) +struct FHyperTwistVisionCorrectionTarget +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReasonId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendedActionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PriorityOrdinal = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bManualReviewPreferred = false; + + bool IsStructurallyValid() const + { + return !FaceId.IsEmpty() + && !ReasonId.IsEmpty() + && !RecommendedActionId.IsEmpty() + && PriorityOrdinal >= 0; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistVisionCorrectionContradiction +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ContradictionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ContradictionKind; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ObservationId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevisionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray AffectedStickerIndices; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBlocksSolveExplanation = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bManualReviewPreferred = false; + + bool IsStructurallyValid() const + { + return !ContradictionId.IsEmpty() + && !FaceId.IsEmpty() + && !ContradictionKind.IsEmpty() + && RevisionCount >= 0; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistVisionCorrectionProfile +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CorrectionProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PuzzleId = TEXT("cube/3x3x3"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CorrectionMode = TEXT("bounded-multi-face-correction-v1"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsTargetedRescan = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsManualCubeEdit = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsStageRestart = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray ActionBindings; + + bool IsStructurallyValid() const + { + if (CorrectionProfileId.IsEmpty() + || PuzzleId.IsEmpty() + || CorrectionMode.IsEmpty() + || ActionBindings.Num() <= 0) + { + return false; + } + + for (const FHyperTwistVisionShellActionBinding& ActionBinding : ActionBindings) + { + if (!ActionBinding.IsStructurallyValid()) + { + return false; + } + } + + return true; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistVisionCorrectionState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CorrectionProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendedStageId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendationStatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendationDetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextRecommendedActionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveTargetFaceId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 MissingFaceCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ContradictionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistVisionCorrectionTarget ActiveTarget; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray PendingTargets; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Contradictions; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCorrectionRequired = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bManualReviewRequired = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bTargetedRescanVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bManualCubeEditVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bStageRestartVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSolveExplanationBlocked = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCorrectionComplete = false; + + bool IsStructurallyValid() const + { + if (CorrectionProfileId.IsEmpty() + || MissingFaceCount < 0 + || ContradictionCount < 0 + || ContradictionCount != Contradictions.Num()) + { + return false; + } + + if (bCorrectionRequired) + { + if (ActiveTargetFaceId.IsEmpty() + || !ActiveTarget.IsStructurallyValid() + || ActiveTargetFaceId != ActiveTarget.FaceId) + { + return false; + } + } + + for (const FHyperTwistVisionCorrectionTarget& Target : PendingTargets) + { + if (!Target.IsStructurallyValid()) + { + return false; + } + } + + for (const FHyperTwistVisionCorrectionContradiction& Contradiction : Contradictions) + { + if (!Contradiction.IsStructurallyValid()) + { + return false; + } + } + + return true; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistVisionSessionConfig { @@ -854,6 +1080,12 @@ struct FHyperTwistVisionSessionConfig UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistVisionSolveExplanationProfile SolveExplanationProfileDefinition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CorrectionProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistVisionCorrectionProfile CorrectionProfileDefinition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FIntPoint FrameSize = FIntPoint(1280, 720); @@ -980,6 +1212,9 @@ struct FHyperTwistVisionCommitResult UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistVisionSolveExplanationState SolveExplanationState; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistVisionCorrectionState CorrectionState; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray Warnings; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubePhase6RRCorrectionContractTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubePhase6RRCorrectionContractTest.cpp new file mode 100644 index 0000000..ce59d9c --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistClassicCubePhase6RRCorrectionContractTest.cpp @@ -0,0 +1,241 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "Misc/AutomationTest.h" + +#include "Engine/GameInstance.h" +#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" +#include "UObject/UnrealType.h" + +#if WITH_AUTOMATION_TESTS + +namespace HyperTwistClassicCubePhase6RRTestInternal +{ + FHyperTwistTrainingDeck MakeRecognitionDeck() + { + FHyperTwistTrainingDeck Deck; + Deck.DeckId = TEXT("phase6r-r/classic-cube-correction-shell"); + Deck.Title = TEXT("Phase 6R-R Classic Cube Correction Shell"); + Deck.DeliveryModes = { + EHyperTwistTrainingDeliveryMode::RecognitionAssisted, + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + + FHyperTwistTrainingCase TrainingCase; + TrainingCase.CaseId = TEXT("phase6r-r-case"); + TrainingCase.PuzzleId = TEXT("cube/3x3x3"); + TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Recognition; + TrainingCase.PromptLabel = TEXT("Phase 6R-R Recognition"); + TrainingCase.AllowedDeliveryModes = { + EHyperTwistTrainingDeliveryMode::RecognitionAssisted, + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + Deck.Cases = {TrainingCase}; + return Deck; + } + + void ForceMockRecognitionClient(UHyperTwistTrainingSubsystem* TrainingSubsystem) + { + if (TrainingSubsystem == nullptr) + { + return; + } + + if (FStrProperty* RecognitionClientKindProperty = FindFProperty( + UHyperTwistTrainingSubsystem::StaticClass(), + TEXT("RecognitionClientKind") + )) + { + RecognitionClientKindProperty->SetPropertyValue_InContainer(TrainingSubsystem, TEXT("mock")); + } + } + + UHyperTwistTrainingSubsystem* MakeRecognitionSubsystem(const FString& SessionId) + { + UGameInstance* GameInstance = NewObject(GetTransientPackage()); + if (GameInstance == nullptr) + { + return nullptr; + } + + UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject(GameInstance); + if (TrainingSubsystem == nullptr) + { + return nullptr; + } + + ForceMockRecognitionClient(TrainingSubsystem); + const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck( + MakeRecognitionDeck(), + TEXT("phase6r-r-user"), + SessionId, + EHyperTwistTrainingDeliveryMode::RecognitionAssisted + ); + return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr; + } + + void SubmitPreviewFrame(UHyperTwistTrainingSubsystem* TrainingSubsystem, const FString& FaceId, const int32 FrameOrdinal) + { + FHyperTwistVisionFrameEnvelope Frame; + Frame.FrameOrdinal = FrameOrdinal; + Frame.TargetFaceHint = FaceId; + TrainingSubsystem->SubmitActiveRecognitionFrame(Frame); + } + + FHyperTwistVisionCommitResult CommitFace( + UHyperTwistTrainingSubsystem* TrainingSubsystem, + const FString& FaceId, + const int32 FrameOrdinal + ) + { + SubmitPreviewFrame(TrainingSubsystem, FaceId, FrameOrdinal); + + FHyperTwistVisionCommitRequest Request; + Request.TargetFace = FaceId; + return TrainingSubsystem->CommitActiveRecognitionObservation(Request); + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubePhase6RRCorrectionSessionConfigTest, + "HyperTwist.Permissive.ClassicCube.Phase6R.R.CorrectionSessionConfig", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubePhase6RRCorrectionSessionConfigTest::RunTest(const FString& Parameters) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistClassicCubePhase6RRTestInternal::MakeRecognitionSubsystem(TEXT("phase6r-r-config-session")); + TestNotNull(TEXT("The recognition subsystem must be constructed for Phase 6R-R."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + FString OpenError; + TestTrue(TEXT("The recognition session must open for the correction shell route."), TrainingSubsystem->OpenActiveRecognitionSession(OpenError)); + TestTrue(TEXT("Opening the recognition session must not report an error."), OpenError.IsEmpty()); + + const FHyperTwistTrainingRecognitionSessionState SessionState = + TrainingSubsystem->GetActiveRecognitionSessionState(); + TestEqual( + TEXT("The correction shell route must normalize the first-party correction profile id."), + SessionState.SessionConfig.CorrectionProfileId, + TEXT("classic-cube-correction-shell-v1") + ); + TestTrue( + TEXT("The correction shell route must provide a structurally valid correction profile definition."), + SessionState.SessionConfig.CorrectionProfileDefinition.IsStructurallyValid() + ); + TestEqual( + TEXT("The correction shell route must retain four bounded correction actions."), + SessionState.SessionConfig.CorrectionProfileDefinition.ActionBindings.Num(), + 4 + ); + TestTrue( + TEXT("The correction shell route must support targeted re-scan in this packet."), + SessionState.SessionConfig.CorrectionProfileDefinition.bSupportsTargetedRescan + ); + TestTrue( + TEXT("The correction shell route must support manual cube edit in this packet."), + SessionState.SessionConfig.CorrectionProfileDefinition.bSupportsManualCubeEdit + ); + TestTrue( + TEXT("The correction shell route must preserve explicit stage-restart support."), + SessionState.SessionConfig.CorrectionProfileDefinition.bSupportsStageRestart + ); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubePhase6RRCorrectionPartialStateTest, + "HyperTwist.Permissive.ClassicCube.Phase6R.R.CorrectionPartialState", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubePhase6RRCorrectionPartialStateTest::RunTest(const FString& Parameters) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistClassicCubePhase6RRTestInternal::MakeRecognitionSubsystem(TEXT("phase6r-r-partial-session")); + TestNotNull(TEXT("The recognition subsystem must be constructed for Phase 6R-R."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + const FHyperTwistVisionCommitResult Result = + HyperTwistClassicCubePhase6RRTestInternal::CommitFace(TrainingSubsystem, TEXT("F"), 1); + + TestTrue(TEXT("The partial correction route must carry a structurally valid correction state."), Result.CorrectionState.IsStructurallyValid()); + TestTrue(TEXT("The partial correction route must keep correction open before capture completes."), Result.CorrectionState.bCorrectionRequired); + TestFalse(TEXT("The partial correction route must not report any contradictions after a clean first face."), Result.CorrectionState.bManualReviewRequired); + TestEqual(TEXT("The partial correction route must target the first missing canonical face next."), Result.CorrectionState.ActiveTargetFaceId, TEXT("U")); + TestEqual(TEXT("The partial correction route must tag the active target as a missing-face correction."), Result.CorrectionState.ActiveTarget.ReasonId, TEXT("missing-face")); + TestEqual(TEXT("The partial correction route must route the next action back to capture."), Result.CorrectionState.NextRecommendedActionId, TEXT("capture-target-face")); + TestEqual(TEXT("The partial correction route must keep the correction stage in video-stream."), Result.CorrectionState.RecommendedStageId, TEXT("video-stream")); + TestEqual(TEXT("The partial correction route must report five faces still missing."), Result.CorrectionState.MissingFaceCount, 5); + TestEqual(TEXT("The partial correction route must report zero contradiction objects after the first clean face."), Result.CorrectionState.ContradictionCount, 0); + TestTrue(TEXT("The partial correction route must keep targeted re-scan or capture visible."), Result.CorrectionState.bTargetedRescanVisible); + TestFalse(TEXT("Solve guidance must remain blocked while correction stays open."), Result.SolveExplanationState.bReadyForExplanation); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistClassicCubePhase6RRCorrectionRevisionStateTest, + "HyperTwist.Permissive.ClassicCube.Phase6R.R.CorrectionRevisionState", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistClassicCubePhase6RRCorrectionRevisionStateTest::RunTest(const FString& Parameters) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistClassicCubePhase6RRTestInternal::MakeRecognitionSubsystem(TEXT("phase6r-r-revision-session")); + TestNotNull(TEXT("The recognition subsystem must be constructed for Phase 6R-R."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + const TArray FaceOrder = { + TEXT("U"), + TEXT("R"), + TEXT("F"), + TEXT("D"), + TEXT("L"), + TEXT("B") + }; + + FHyperTwistVisionCommitResult Result; + for (int32 FaceOrdinal = 0; FaceOrdinal < FaceOrder.Num(); ++FaceOrdinal) + { + Result = HyperTwistClassicCubePhase6RRTestInternal::CommitFace( + TrainingSubsystem, + FaceOrder[FaceOrdinal], + FaceOrdinal + 1 + ); + } + Result = HyperTwistClassicCubePhase6RRTestInternal::CommitFace(TrainingSubsystem, TEXT("F"), 7); + + const FHyperTwistTrainingRecognitionSessionState SessionState = + TrainingSubsystem->GetActiveRecognitionSessionState(); + TestTrue(TEXT("The revision route must keep a structurally valid reconstruction session."), Result.ReconstructionSession.IsStructurallyValid()); + TestTrue(TEXT("The revision route must keep the classic cube net complete after a face recommit."), Result.ReconstructionSession.bHasCompleteClassicCubeNet); + TestEqual(TEXT("The revision route must record exactly one bounded reconstruction revision."), Result.ReconstructionSession.TotalRevisionCount, 1); + TestTrue(TEXT("The revision route must carry a structurally valid correction state."), Result.CorrectionState.IsStructurallyValid()); + TestTrue(TEXT("The revision route must reopen correction after a face recommit."), Result.CorrectionState.bCorrectionRequired); + TestTrue(TEXT("The revision route must require manual review after a full-net recommit."), Result.CorrectionState.bManualReviewRequired); + TestTrue(TEXT("The revision route must expose manual cube edit once review is required."), Result.CorrectionState.bManualCubeEditVisible); + TestFalse(TEXT("The revision route must not report correction complete while review is still open."), Result.CorrectionState.bCorrectionComplete); + TestEqual(TEXT("The revision route must keep the recommitted face as the active correction target."), Result.CorrectionState.ActiveTargetFaceId, TEXT("F")); + TestEqual(TEXT("The revision route must classify the active correction as a revised-face target."), Result.CorrectionState.ActiveTarget.ReasonId, TEXT("revised-face")); + TestEqual(TEXT("The revision route must hand the next action to manual correction once the full net exists."), Result.CorrectionState.NextRecommendedActionId, TEXT("apply-manual-corrections")); + TestEqual(TEXT("The revision route must switch the correction stage into cube-edit."), Result.CorrectionState.RecommendedStageId, TEXT("cube-edit")); + TestEqual(TEXT("The revision route must surface one contradiction object for the recommit."), Result.CorrectionState.ContradictionCount, 1); + TestEqual(TEXT("The revision route must tag the contradiction as a manual revision."), Result.CorrectionState.Contradictions[0].ContradictionKind, TEXT("manual-revision")); + TestFalse(TEXT("Solve guidance must stay blocked until the correction review closes."), Result.SolveExplanationState.bReadyForExplanation); + TestFalse(TEXT("The solve explanation start action must stay hidden while correction is open."), Result.SolveExplanationState.bStartExplanationVisible); + TestEqual(TEXT("The solve explanation route must redirect back to cube-edit while correction is open."), Result.SolveExplanationState.RecommendedStageId, TEXT("cube-edit")); + TestEqual(TEXT("A bounded face recommit must still count as one correction event."), SessionState.CorrectionEventCount, 1); + return true; +} + +#endif diff --git a/docs/REPO_LICENSE_TRACKING.md b/docs/REPO_LICENSE_TRACKING.md index 3a6475e..d5c44cc 100644 --- a/docs/REPO_LICENSE_TRACKING.md +++ b/docs/REPO_LICENSE_TRACKING.md @@ -157,8 +157,11 @@ Status update on `2026-05-21`: solve explanation/recommendation packet is now landed in current code - the generic source-backed `Phase 6R-R` shared classic-cube recognition multi-face correction/explanation control pass is now consumed -- the current next bounded move is the bounded permissive `Phase 6R-R` classic-cube recognition - multi-face correction/explanation shell packet, not a new restrictive packet by default +- the bounded permissive `Phase 6R-R` shared classic-cube recognition multi-face + correction/explanation shell packet is now landed in current code +- the current next bounded move is a source-backed follow-on control pass for the retained + classic-cube recognition correction/explanation shell remainder, not a new restrictive packet by + default - use the repo-row README census, the portfolio standing refresh backfill, and the `2R-A` ownership contract for the current queue after that correction @@ -262,8 +265,8 @@ Current practical interpretation: - the landed `PostHog/posthog` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_D_POSTHOG_CONTROL_PLANE_IMPLEMENTATION_2026-05-13.md` - the landed `screenpipe/screenpipe` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_E_SCREENPIPE_CAPTURE_HISTORY_IMPLEMENTATION_2026-05-13.md` - the landed `remotion-dev/remotion` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_F_REMOTION_MEDIA_EXPORT_IMPLEMENTATION_2026-05-13.md` -- the next bounded move is the bounded permissive `Phase 6R-R` classic-cube recognition - multi-face correction/explanation shell packet +- the next bounded move is a source-backed follow-on control pass for the retained classic-cube + recognition correction/explanation shell remainder Companion docs: @@ -752,10 +755,11 @@ Approved working posture: - classic-cube recognition calibration contract - ordered face-observation contract - classic-cube webcam shell + - shared correction-target capture-state and re-scan adjunct via `Phase 6R-R` - keep the following families deferred to later queue decisions: - bundled-font redistribution and multilingual solve shell - - direct standalone multi-face reconstruction / correction / explanation - - only the shared `Phase 6R-R` capture-state adjunct remains live now + - direct standalone multilingual or asset-shipping widening beyond the landed shared correction + adjunct - if later implementation copies bundled non-code assets, capture and preserve any asset-specific license or replace the asset with a clearly redistributable alternative @@ -807,9 +811,11 @@ Approved working posture: - final classic-net shaping above aggregated committed faces - browser/webcam shell profile and browser-shell session-state composition - bounded solve explanation/recommendation stage ladder and recommendation state + - bounded correction profile, contradiction-aware correction state, and + correction-explanation shell routing - keep the row only partially incorporated by default: - - multi-face correction or explanation shell beyond the bounded stage ladder is the queued - `Phase 6R-R` implementation slice + - retained correction/explanation shell remainder beyond the landed + correction-state slice still requires a separate control decision - any redistribution of `frontend/lib/twistysim.min.js` still requires preserved or replaced upstream provenance/license diff --git a/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_IMPLEMENTATION_PACKET_2026-05-24.md b/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_IMPLEMENTATION_PACKET_2026-05-24.md new file mode 100644 index 0000000..641b8a7 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_IMPLEMENTATION_PACKET_2026-05-24.md @@ -0,0 +1,169 @@ +# HyperTwist Phase 6R-R classic-cube multi-face correction explanation implementation packet + +Created on `2026-05-24` + +## Status + +- first-party HyperTwist packet +- bounded permissive `Phase 6R-R` implementation slice + +## Purpose + +This packet lands the current retained shared classic-cube recognition +correction/explanation family: + +- first-party correction-profile, correction-target, contradiction, and + correction-explanation shell composition above the already landed `qbr` + webcam shell plus the already landed `rubix-cube-solver` reconstruction, + browser-shell, and bounded solve-explanation seams + +It is not: + +- a full `qbr` row transplant +- a full `rubix-cube-solver` row transplant +- a bundled `twistysim.min.js` redistribution packet +- a bundled `arial-unicode-ms.ttf` redistribution packet +- a multilingual `qbr` shell packet +- a generalized solver backend packet +- a broad playback-runtime packet + +## Current authority basis + +This implementation packet stands on: + +- `docs/REPO_LICENSE_TRACKING.md` +- `docs/ops/HYPERTWIST_CROSS_LANE_AUTHORITY_HIERARCHY_AND_RECONCILIATION_2026-05-20.md` +- `docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_PREPARATION_PACKET_2026-05-23.md` + +The retained owner split stays explicit: + +- `vivaansinghvi07/rubix-cube-solver` + - retained primary owner for correction/explanation semantics, revision + review, contradiction visibility, and manual-correction routing +- `kkoomen/qbr` + - retained adjunct owner for capture-state continuity, target-face re-scan + anchoring, preview/snapshot discipline, and the shared correction shell's + live recognition posture + +The top-level product shell owner does not change here: + +- first-party HyperTwist remains the broader recognition-session and + product-shell owner + +## Landed scope + +The current code now owns a bounded first-party correction shell through: + +- first-party `HyperTwistRecognition` contract types for: + - correction target definition + - contradiction object definition + - correction profile definition + - correction session-state readout + - correction profile id on recognition session config + - commit-result correction payloads +- recognition session config defaults for: + - first-party classic-cube correction profile id + - correction profile definition with explicit bounded action bindings +- first-party commit-result shaping for: + - missing-face correction routing + - conflicting-face contradiction objects + - recommitted-face contradiction objects + - low-confidence face targeting + - targeted re-scan, manual cube edit, and stage-restart visibility + - explicit correction-complete versus solve-ready gating + - solve-explanation blocking and stage redirect once a full net exists but + correction remains open +- sample contract routing in: + - `UHyperTwistContractLibrary` +- active subsystem normalization in: + - `UHyperTwistTrainingSubsystem` +- focused automation coverage in: + - `HyperTwistClassicCubePhase6RRCorrectionContractTest.cpp` + +## Why this is still intentionally bounded + +This packet lands the first bounded correction-state and +correction-explanation shell slice, but it does not widen into neighboring +families or broader ownership. + +Still excluded: + +- bundled `twistysim.min.js` redistribution +- bundled `arial-unicode-ms.ttf` redistribution +- standalone multilingual `qbr` shell ownership +- generalized solver backend ownership +- broad playback-runtime ownership +- donor-shaped frontend transplant + +## Validation + +Build validation: + +- `C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\Build.bat UnrealHyperTwistEditor Win64 Development -Project='C:\HyperTwist\UnrealHyperTwist\UnrealHyperTwist.uproject' -WaitMutex -NoHotReloadFromIDE -NoUba` + +Focused automation validation: + +- `C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe C:\HyperTwist\UnrealHyperTwist\UnrealHyperTwist.uproject -unattended -nop4 -nosplash -NullRHI -log -stdout -FullStdOutLogOutput -AbsLog=C:\HyperTwist\UnrealHyperTwist\Saved\Logs\Phase6R-R-ClassicCube-Verify.log -ReportExportPath=C:\HyperTwist\UnrealHyperTwist\Saved\AutomationReports\Phase6R-R-ClassicCube-Verify -ExecCmds="Automation RunTests HyperTwist.Permissive.ClassicCube.Phase6R.R; Quit" -TestExit="Automation Test Queue Empty"` + +Regression automation validation: + +- `HyperTwist.Permissive.RubixCubeSolver.Phase6R.Q` +- `HyperTwist.Permissive.RubixCubeSolver.Phase6R.P` +- `HyperTwist.Permissive.Qbr.Phase6R.O` +- `HyperTwist.Permissive.RubixCubeSolver.Phase6R.C` +- `HyperTwist.CleanRoom.CubeDesk` + +Expected covered tests: + +- `CorrectionSessionConfig` +- `CorrectionPartialState` +- `CorrectionRevisionState` +- existing `rubix-cube-solver`, `qbr`, and `CubeDesk` regression suites + +## Queue effect + +This packet consumes the current bounded `Phase 6R-R` implementation slice. + +`kkoomen/qbr` remains partially incorporated: + +- landed now: + - classic-cube recognition calibration contract + - ordered face-observation contract + - classic-cube webcam shell + - shared correction-target capture-state and re-scan adjunct +- still deferred: + - bundled-font redistribution and multilingual solve shell + +`vivaansinghvi07/rubix-cube-solver` remains partially incorporated: + +- landed now: + - committed-face reconstruction session + - face-vote replacement ledger + - final classic-net shaping above aggregated committed faces + - browser/webcam shell profile and browser-shell session-state composition + - bounded solve explanation/recommendation stage ladder and recommendation + state + - bounded correction profile, contradiction-aware correction state, and + correction-explanation shell routing +- still deferred: + - retained correction/explanation shell remainder beyond the landed + correction-state slice + - bundled `twistysim.min.js` redistribution + +The next clean move is: + +- a source-backed follow-on control pass for the retained classic-cube + recognition correction/explanation shell remainder shared across + `kkoomen/qbr` and `vivaansinghvi07/rubix-cube-solver` + +Keep the future sequencing guards visible: + +- do not copy or redistribute `frontend/lib/twistysim.min.js` without + preserving or replacing its upstream provenance/license in any future + widening that would ship that asset +- do not copy or redistribute `src/assets/arial-unicode-ms.ttf` without + separate confirmation or replacement in any future widening that would ship + that asset +- do not widen into broad playback-runtime or generalized solver-backend + ownership unless a narrower first-party gap is proven above the landed + correction shell diff --git a/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_PREPARATION_PACKET_2026-05-23.md b/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_PREPARATION_PACKET_2026-05-23.md index d76165b..6bb6bc5 100644 --- a/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_PREPARATION_PACKET_2026-05-23.md +++ b/docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_PREPARATION_PACKET_2026-05-23.md @@ -6,7 +6,8 @@ Created on `2026-05-23` - historical consumed preparation authority - bounded post-`Phase 6R-Q` control slice -- the first bounded implementation slice now lands separately after this packet +- the first bounded implementation slice now lands in: + - `docs/arch/HYPERTWIST_PHASE6R_R_CLASSIC_CUBE_MULTI_FACE_CORRECTION_EXPLANATION_IMPLEMENTATION_PACKET_2026-05-24.md` ## Purpose diff --git a/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md b/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md index 64a05dd..bc70ae4 100644 --- a/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md +++ b/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md @@ -106,13 +106,15 @@ The next bounded move is now: preset-backed overrideable context-assembly profiles rather than a second memory system 9. the generic source-backed `Phase 6R-R` shared classic-cube recognition multi-face correction/explanation control pass is now consumed -10. the next bounded move is the bounded permissive `Phase 6R-R` classic-cube recognition - multi-face correction/explanation shell packet shared across `kkoomen/qbr` and - `vivaansinghvi07/rubix-cube-solver` -11. keep the `rubix-cube-solver` guard visible: +10. the bounded permissive `Phase 6R-R` shared classic-cube recognition multi-face + correction/explanation shell packet is now landed in current code +11. the next bounded move is a source-backed follow-on control pass for the retained + classic-cube recognition correction/explanation shell remainder shared across `kkoomen/qbr` + and `vivaansinghvi07/rubix-cube-solver` +12. keep the `rubix-cube-solver` guard visible: - do not copy or redistribute `frontend/lib/twistysim.min.js` without preserving or replacing its upstream provenance/license in any future widening that would ship that asset -12. keep the `qbr` guard visible: +13. keep the `qbr` guard visible: - do not copy or redistribute `src/assets/arial-unicode-ms.ttf` without separate confirmation or replacement in any future widening that would ship that asset diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index 9953d7d..5e7f30d 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -151,7 +151,7 @@ repo. | Bounded solve explanation/recommendation shell | Implemented now | `rubix-cube-solver` bounded packet | Live stage-ladder recommendation state above the bounded browser recognition shell. | | Provider-backed recognition service contract | Implemented now | first-party recognition client surfaces | First-party normalized recognition session boundary is real. | | Multilingual recognition guidance shell | Deep-source grounded retained | `qbr` retained remainder | Kept deferred until a font-safe and provenance-safe shipping path is explicit. | -| Multi-face correction/explanation shell | Deep-source grounded retained | `rubix-cube-solver` with `qbr` capture-state adjunct value | Shared retained shell is source-backed, but keep it above the landed `Phase 6R-O`, `Phase 6R-P`, and `Phase 6R-Q` seams and keep both bundled-asset guards explicit. | +| Multi-face correction/explanation shell | Implemented now | first-party recognition/training surfaces | Current bounded correction profile, contradiction-aware correction state, and correction-explanation shell are live above the landed `Phase 6R-O`, `Phase 6R-P`, and `Phase 6R-Q` seams. Broader playback/runtime and bundled-asset widening remain deferred. | ### 3. Training, coaching, and progression cockpit diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index 2ebdfa5..1068e9b 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -133,9 +133,11 @@ Canonical discovery surfaces for roadmap interpretation: preset-backed overrideable context-assembly profiles rather than a second memory system - the generic source-backed `Phase 6R-R` shared classic-cube recognition multi-face correction/explanation control pass is now consumed -- the current next bounded move is the bounded permissive `Phase 6R-R` classic-cube recognition - multi-face correction/explanation shell packet, while keeping both the `twistysim.min.js` and - `arial-unicode-ms.ttf` redistribution guards explicit +- the bounded permissive `Phase 6R-R` shared classic-cube recognition multi-face + correction/explanation shell packet is now landed in current code +- the current next bounded move is a source-backed follow-on control pass for the retained + classic-cube recognition correction/explanation shell remainder, while keeping both the + `twistysim.min.js` and `arial-unicode-ms.ttf` redistribution guards explicit - the repo-row implementation queue is now live from that `Phase 6R-A` entry point rather than waiting on another first-party packet @@ -197,11 +199,11 @@ Current routing truth: - active non-live implementation-board rows: `34` - retained benchmark, oracle, or clean-room-later rows outside the active implementation board: `9` -The next bounded move is the bounded permissive `Phase 6R-R` classic-cube recognition -multi-face correction/explanation shell packet shared across `kkoomen/qbr` and +The next bounded move is a source-backed follow-on control pass for the retained classic-cube +recognition correction/explanation shell remainder shared across `kkoomen/qbr` and `vivaansinghvi07/rubix-cube-solver`. -Queue interpretation after that packet: +Queue interpretation after that control pass: - then continue with the retained repo-row queue - highest current repo-row queue pressure sits in: @@ -221,10 +223,9 @@ Queue interpretation after that packet: - classic-cube recognition calibration contract - ordered face-observation contract - classic-cube webcam UI shell + - shared correction-target capture-state and re-scan adjunct - still deferred: - bundled-font redistribution and multilingual solve shell - - no standalone correction packet remains; the surviving adjacent value is - the shared `Phase 6R-R` capture-state adjunct - `roice3/Magic120Cell` remains a partially landed row rather than a closed row: - landed: - dedicated `120-cell` family runtime profile @@ -254,10 +255,14 @@ Queue interpretation after that packet: - final classic-net shaping above aggregated committed faces - browser/webcam shell profile and browser-shell session-state composition - bounded solve explanation/recommendation stage ladder and recommendation state + - bounded correction profile, contradiction-aware correction state, and + correction-explanation shell routing - still deferred: - - multi-face correction or explanation shell beyond the bounded stage ladder + - retained correction/explanation shell remainder beyond the landed + correction-state slice - bundled `twistysim.min.js` redistribution -- after the `Phase 6R-Q` implementation packet, keep the future legal sequencing guards visible: +- after the landed `Phase 6R-R` implementation packet, keep the future legal sequencing guards + visible: - `vivaansinghvi07/rubix-cube-solver` - do not copy or redistribute `frontend/lib/twistysim.min.js` without preserving or replacing its upstream provenance/license in any future widening that would ship that asset @@ -267,7 +272,7 @@ Queue interpretation after that packet: - `Phase 6R-R` - keep `vivaansinghvi07/rubix-cube-solver` as the retained correction/explanation-semantic owner and preserve `kkoomen/qbr` as the retained capture-state and re-scan adjunct without - widening into either bundled asset family + widening into either bundled asset family or broad playback/runtime ownership - `roice3/MagicTile` remains a partially landed row rather than a closed row: - landed: - tiling topology and geometry-family contract @@ -332,11 +337,11 @@ Queue interpretation after that packet: - viseme / gesture runtime integration - broad assistant-platform scope - the next queue shape is now: - - retained classic-cube recognition multi-face correction/explanation shell remainder -- the next bounded packet should stay narrow: - - first bounded `Phase 6R-R` correction-state and correction-explanation shell widening before - bundled `twistysim.min.js` redistribution, generalized solver backend ownership, or any `qbr` - multilingual/font-shipping path + - retained classic-cube recognition correction/explanation shell remainder assessment +- the next bounded move should stay narrow: + - a source-backed follow-on control pass before any widening into bundled `twistysim.min.js` + redistribution, generalized solver backend ownership, any `qbr` multilingual/font-shipping + path, or broad playback-runtime ownership - keep the speech-input / voice sidecar legal sequencing guard visible: - keep code-license judgments separate from model, voice, and payload-license review - keep the provider-neutral speech-lane guard visible: