Harden package validation and refresh Hyperspeedcube evidence

This commit is contained in:
axiomlogicnexus 2026-06-18 21:22:41 +00:00
parent fb72fbc2c8
commit c2b1d0a030
12 changed files with 610 additions and 128 deletions

View file

@ -32,3 +32,6 @@ ProfilesPath=/voice/profiles
SynthesizePath=/voice/narrate
HealthPath=/voice/health
RequestTimeoutSeconds=5.0
[/Script/DeveloperToolSettings.ProjectPackagingSettings]
+DirectoriesToNeverCook=(Path="/MovieRenderPipeline/Blueprints")

View file

@ -89,6 +89,125 @@ namespace HyperTwistReplayPersistenceLibraryInternal
FMath::Max(Sequence, 1)
);
}
FString BuildFallbackPacketIdentity(const FHyperTwistReplayPacket& ReplayPacket)
{
const FString PuzzleToken = SanitizePathToken(
!ReplayPacket.PuzzleDefinition.PuzzleId.IsEmpty()
? ReplayPacket.PuzzleDefinition.PuzzleId
: TEXT("replay")
);
const FString IdentitySeed = !ReplayPacket.StartedAtUtc.IsEmpty()
? ReplayPacket.StartedAtUtc
: !ReplayPacket.EndedAtUtc.IsEmpty()
? ReplayPacket.EndedAtUtc
: FString::Printf(TEXT("event-count-%d"), ReplayPacket.Events.Num());
return FString::Printf(
TEXT("%s_%s"),
*PuzzleToken,
*SanitizePathToken(IdentitySeed)
);
}
bool ShouldSortEventsBySequence(const TArray<FHyperTwistReplayEvent>& Events)
{
if (Events.Num() < 2)
{
return false;
}
for (const FHyperTwistReplayEvent& ReplayEvent : Events)
{
if (ReplayEvent.Sequence <= 0)
{
return false;
}
}
return true;
}
void NormalizeEventOrder(TArray<FHyperTwistReplayEvent>& Events)
{
if (!ShouldSortEventsBySequence(Events))
{
return;
}
struct FIndexedReplayEvent
{
FHyperTwistReplayEvent Event;
int32 OriginalIndex = 0;
};
TArray<FIndexedReplayEvent> IndexedEvents;
IndexedEvents.Reserve(Events.Num());
for (int32 EventIndex = 0; EventIndex < Events.Num(); ++EventIndex)
{
FIndexedReplayEvent& IndexedEvent = IndexedEvents.AddDefaulted_GetRef();
IndexedEvent.Event = Events[EventIndex];
IndexedEvent.OriginalIndex = EventIndex;
}
IndexedEvents.Sort([](const FIndexedReplayEvent& Left, const FIndexedReplayEvent& Right)
{
if (Left.Event.Sequence != Right.Event.Sequence)
{
return Left.Event.Sequence < Right.Event.Sequence;
}
if (Left.Event.TimeMs != Right.Event.TimeMs)
{
return Left.Event.TimeMs < Right.Event.TimeMs;
}
return Left.OriginalIndex < Right.OriginalIndex;
});
Events.Reset();
Events.Reserve(IndexedEvents.Num());
for (FIndexedReplayEvent& IndexedEvent : IndexedEvents)
{
Events.Add(MoveTemp(IndexedEvent.Event));
}
}
FString BuildUniqueEventId(
const FHyperTwistReplayEvent& ReplayEvent,
const int32 Sequence,
TSet<FString>& UsedEventIds
)
{
const FString PreferredEventId = !ReplayEvent.EventId.IsEmpty()
? ReplayEvent.EventId
: BuildFallbackEventId(ReplayEvent, Sequence);
if (!UsedEventIds.Contains(PreferredEventId))
{
UsedEventIds.Add(PreferredEventId);
return PreferredEventId;
}
const FString BaseEventId = SanitizePathToken(PreferredEventId);
FString CandidateEventId = FString::Printf(
TEXT("%s_%03d"),
*BaseEventId,
FMath::Max(Sequence, 1)
);
int32 CollisionIndex = 1;
while (UsedEventIds.Contains(CandidateEventId))
{
CandidateEventId = FString::Printf(
TEXT("%s_%03d_%02d"),
*BaseEventId,
FMath::Max(Sequence, 1),
CollisionIndex
);
++CollisionIndex;
}
UsedEventIds.Add(CandidateEventId);
return CandidateEventId;
}
}
FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory()
@ -117,10 +236,24 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack
NormalizedPacket.SessionId = NormalizedPacket.ReplayId;
}
if (NormalizedPacket.ReplayId.IsEmpty() && NormalizedPacket.SessionId.IsEmpty())
{
const FString FallbackIdentity =
HyperTwistReplayPersistenceLibraryInternal::BuildFallbackPacketIdentity(
NormalizedPacket
);
NormalizedPacket.ReplayId = FallbackIdentity;
NormalizedPacket.SessionId = FallbackIdentity;
}
HyperTwistReplayPersistenceLibraryInternal::NormalizeEventOrder(NormalizedPacket.Events);
int32 PreviousSequence = 0;
int32 PreviousTimeMs = 0;
TSet<FString> UsedEventIds;
for (FHyperTwistReplayEvent& ReplayEvent : NormalizedPacket.Events)
{
ReplayEvent.TimeMs = FMath::Max(ReplayEvent.TimeMs, 0);
if (ReplayEvent.Sequence <= PreviousSequence)
{
ReplayEvent.Sequence = PreviousSequence + 1;
@ -131,19 +264,12 @@ FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPack
ReplayEvent.TimeMs = PreviousTimeMs;
}
if (ReplayEvent.TimeMs < 0)
{
ReplayEvent.TimeMs = 0;
}
if (ReplayEvent.EventId.IsEmpty())
{
ReplayEvent.EventId =
HyperTwistReplayPersistenceLibraryInternal::BuildFallbackEventId(
ReplayEvent,
ReplayEvent.Sequence
);
}
ReplayEvent.EventId =
HyperTwistReplayPersistenceLibraryInternal::BuildUniqueEventId(
ReplayEvent,
ReplayEvent.Sequence,
UsedEventIds
);
PreviousSequence = ReplayEvent.Sequence;
PreviousTimeMs = ReplayEvent.TimeMs;

View file

@ -8,6 +8,8 @@
namespace HyperTwistClassicCubeLeaderboardLibraryInternal
{
const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1");
const TCHAR* DefaultPuzzleId = TEXT("cube/3x3x3");
const TCHAR* FallbackRecordedAtUtc = TEXT("9999-12-31T23:59:59Z");
template <typename TStruct>
bool SerializeStruct(const TStruct& Value, FString& OutJson)
@ -42,6 +44,25 @@ namespace HyperTwistClassicCubeLeaderboardLibraryInternal
return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder);
}
FHyperTwistClassicCubeLeaderboardEntry NormalizeEntry(
const FHyperTwistClassicCubeLeaderboardEntry& Entry
)
{
FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = Entry;
if (NormalizedEntry.PuzzleId.IsEmpty())
{
NormalizedEntry.PuzzleId = DefaultPuzzleId;
}
NormalizedEntry.ScrambleLength = FMath::Max(NormalizedEntry.ScrambleLength, 0);
if (NormalizedEntry.RecordedAtUtc.IsEmpty())
{
NormalizedEntry.RecordedAtUtc = FallbackRecordedAtUtc;
}
return NormalizedEntry;
}
bool ShouldPreferEntry(
const FHyperTwistClassicCubeLeaderboardEntry& Candidate,
const FHyperTwistClassicCubeLeaderboardEntry& Existing
@ -164,27 +185,29 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
TMap<FString, FHyperTwistClassicCubeLeaderboardEntry> BestEntryByKey;
for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries)
{
if (!Entry.IsStructurallyValid())
const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
if (!NormalizedEntry.IsStructurallyValid())
{
continue;
}
const FString Key = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
Entry.PuzzleId,
Entry.ScrambleLength
NormalizedEntry.PuzzleId,
NormalizedEntry.ScrambleLength
);
if (FHyperTwistClassicCubeLeaderboardEntry* ExistingEntry = BestEntryByKey.Find(Key))
{
if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
Entry,
NormalizedEntry,
*ExistingEntry))
{
*ExistingEntry = Entry;
*ExistingEntry = NormalizedEntry;
}
}
else
{
BestEntryByKey.Add(Key, Entry);
BestEntryByKey.Add(Key, NormalizedEntry);
}
}
@ -267,14 +290,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
UpdatedState = FHyperTwistClassicCubeLeaderboardState();
UpdatedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion;
}
if (!Entry.IsStructurallyValid())
const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
if (!NormalizedEntry.IsStructurallyValid())
{
return UpdatedState;
}
const FString TargetKey = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
Entry.PuzzleId,
Entry.ScrambleLength
NormalizedEntry.PuzzleId,
NormalizedEntry.ScrambleLength
);
const int32 ExistingIndex = UpdatedState.Entries.IndexOfByPredicate(
[&TargetKey](const FHyperTwistClassicCubeLeaderboardEntry& Candidate)
@ -288,16 +313,16 @@ FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary:
if (ExistingIndex == INDEX_NONE)
{
UpdatedState.Entries.Add(Entry);
UpdatedState.Entries.Add(NormalizedEntry);
}
else
{
const FHyperTwistClassicCubeLeaderboardEntry& ExistingEntry = UpdatedState.Entries[ExistingIndex];
if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
Entry,
NormalizedEntry,
ExistingEntry))
{
UpdatedState.Entries[ExistingIndex] = Entry;
UpdatedState.Entries[ExistingIndex] = NormalizedEntry;
}
}

View file

@ -30,7 +30,15 @@ namespace
int32 DetermineSolverThreadCount()
{
const int32 AvailablePhysicalCores = FMath::Max(1, FPlatformMisc::NumberOfCores());
return FMath::Clamp(AvailablePhysicalCores / 2, 1, 4);
return FMath::Clamp(AvailablePhysicalCores - 2, 1, 8);
}
int32 DetermineSolverSplitCount(const int32 ThreadCount)
{
// Single-split search leaves short classic-cube solves badly under-distributed on
// multicore hosts. Split phase-1 roots more aggressively so guidance/search work
// can saturate the available worker pool.
return FMath::Clamp(ThreadCount * 3, 1, move::COUNT1);
}
bool TryResolveMoveIndex(const FString& MoveString, int32& OutMoveIndex)
@ -114,7 +122,9 @@ TArray<FString> UHyperTwistSolverLibrary::SolveClassicState(
return Result;
}
solve::Engine Solver(DetermineSolverThreadCount(), TimeLimitMs, NumSolutions, MaxLength, 1);
const int32 ThreadCount = DetermineSolverThreadCount();
const int32 SplitCount = DetermineSolverSplitCount(ThreadCount);
solve::Engine Solver(ThreadCount, TimeLimitMs, NumSolutions, MaxLength, SplitCount);
Solver.prepare();
std::vector<std::vector<int>> Solutions;

View file

@ -576,18 +576,65 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
)
),
MakePackageReference(
TEXT("package/hyperspeedcube-hps-ft-cube-example"),
TEXT("hps/puzzles/ft_cube.hps"),
TEXT("./hps/puzzles/ft_cube.hps"),
TEXT("hyper-dsl-builtin-authoring-example"),
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
TEXT("crates/hyperpuzzle/src/lib.rs"),
TEXT("./crates/hyperpuzzle/src/lib.rs"),
TEXT("hyper-dsl-symmetric-engine-registration-support"),
TEXT("direct dual-license donor"),
TEXT("Retained built-in authoring example for facet-turning cube generators, parameter lists, puzzle tags, example metadata, twist-system declarations, and vantage declarations beneath the bounded HyperTwist DSL lane."),
TEXT("Retained symmetric HPS registration seam for wiring the general symmetric puzzle backend into the bounded HyperTwist DSL lane without transferring the broader donor runtime anchor."),
{
TEXT("add_puzzle_generator example"),
TEXT("parameterized generator example"),
TEXT("example alias tags"),
TEXT("twist-system declaration example"),
TEXT("vantage-set declaration example")
TEXT("symmetric puzzle engine registration"),
TEXT("symmetric twist-system engine registration"),
TEXT("symmetric builtins define_in"),
TEXT("symmetric catalog add_puzzles")
},
CommonNoticeActions,
{
TEXT("whole puzzle engine ownership"),
TEXT("whole runtime-anchor ownership"),
TEXT("viewer or editor shell ownership")
},
MakeAttribution(
TEXT("hyperspeedcube/dsl-symmetric-registration"),
TEXT("Preserves the retained symmetric HPS registration seam without promoting the donor runtime anchor, puzzle engine, or viewer shell into first-party ownership.")
)
),
MakePackageReference(
TEXT("package/hyperspeedcube-hps-symmetric-engine"),
TEXT("crates/hyperpuzzle_impl_symmetric/src/lib.rs"),
TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs"),
TEXT("hyper-dsl-symmetric-engine-support"),
TEXT("direct dual-license donor"),
TEXT("Retained general symmetric puzzle backend seam for HPS-defined symmetric puzzle families, orbit-aware naming, and catalog population beneath the bounded HyperTwist DSL lane."),
{
TEXT("general symmetric puzzle backend"),
TEXT("symmetric add_puzzles_to_catalog"),
TEXT("symmetric orbit autonaming"),
TEXT("symmetric family puzzle build")
},
CommonNoticeActions,
{
TEXT("whole runtime-anchor ownership"),
TEXT("whole puzzle corpus ownership"),
TEXT("donor shell ownership")
},
MakeAttribution(
TEXT("hyperspeedcube/dsl-symmetric-engine"),
TEXT("Preserves the retained symmetric puzzle backend seam as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.")
)
),
MakePackageReference(
TEXT("package/hyperspeedcube-hps-symmetric-platonic-example"),
TEXT("hps/symmetric/4d_platonic.hps"),
TEXT("./hps/symmetric/4d_platonic.hps"),
TEXT("hyper-dsl-symmetric-authoring-example"),
TEXT("direct dual-license donor"),
TEXT("Retained symmetric HPS authoring example for built-in shape-family imports, symmetric engine declarations, and higher-dimensional puzzle-family catalog composition beneath the bounded HyperTwist DSL lane."),
{
TEXT("symmetric shape-family example"),
TEXT("4D platonic family example"),
TEXT("symmetric engine declaration example"),
TEXT("built-in shape import example")
},
CommonNoticeActions,
{
@ -596,8 +643,8 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
TEXT("donor shell ownership")
},
MakeAttribution(
TEXT("hyperspeedcube/dsl-ft-cube-example"),
TEXT("Preserves the retained FT-cube built-in authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime, or donor shell ownership.")
TEXT("hyperspeedcube/dsl-symmetric-platonic-example"),
TEXT("Preserves the retained symmetric HPS authoring example as bounded DSL evidence without transferring whole puzzle-corpus, runtime-anchor, or donor shell ownership.")
)
)
};
@ -875,7 +922,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
TEXT("package/hyperspeedcube-hps-engines"),
TEXT("package/hyperspeedcube-hps-catalog-builtins"),
TEXT("package/hyperspeedcube-hps-codegen"),
TEXT("package/hyperspeedcube-hps-ft-cube-example")
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
TEXT("package/hyperspeedcube-hps-symmetric-engine"),
TEXT("package/hyperspeedcube-hps-symmetric-platonic-example")
};
DslContract.SyntaxSurfaceTags =
{
@ -897,7 +946,9 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
TEXT("tag inheritance on generated puzzles"),
TEXT("engine kwarg routing"),
TEXT("orbit HPS code emission"),
TEXT("add_puzzle_generator example")
TEXT("add_puzzle_generator example"),
TEXT("symmetric shape-family example"),
TEXT("symmetric engine registration")
};
DslContract.DiagnosticSurfaceTags =
{
@ -909,7 +960,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
DslContract.ProvenanceCaveats =
{
TEXT("the retained Hyperpuzzlescript DSL stays bounded to authoring and diagnostics rather than transferring whole runtime-anchor ownership"),
TEXT("the retained FT-cube file is evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"),
TEXT("the retained symmetric HPS example and backend registration are evidence for bounded authoring shape rather than a transfer of whole puzzle-corpus ownership"),
TEXT("notation, replay verification, stats-shape, and broader hyper-runtime ownership remain separately packetized even when the same donor row supplies adjacent evidence")
};
DslContract.ExplicitExclusions =
@ -937,6 +988,7 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
TEXT("package/hyperspeedcube-hps-modules"),
TEXT("package/hyperspeedcube-hps-runtime-core"),
TEXT("package/hyperspeedcube-hps-engines"),
TEXT("package/hyperspeedcube-hps-symmetric-registration"),
TEXT("package/hyperspeedcube-hps-request-bridge")
};
DslBoundary.ModuleSurfaceTags =
@ -954,15 +1006,19 @@ UHyperTwistTrainingHyperspeedcubeLibrary::BuildBundledHyperspeedcubeReferenceBun
TEXT("builtins scope injection"),
TEXT("puzzle engine registry"),
TEXT("twist-system engine registry"),
TEXT("symmetric puzzle engine registration"),
TEXT("symmetric twist-system engine registration"),
TEXT("eval request channel"),
TEXT("blocking HPS-thread eval"),
TEXT("non-reentrant eval rule"),
TEXT("parse_all preload"),
TEXT("exec_all_files preload")
TEXT("exec_all_files preload"),
TEXT("symmetric catalog add_puzzles")
};
DslBoundary.ProvenanceCaveats =
{
TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party"),
TEXT("the current upstream HPS lane registers both nd-euclid and symmetric engines without transferring whole runtime ownership"),
TEXT("engine callbacks defer object construction until build time and do not transfer whole runtime ownership"),
TEXT("blocking eval requests are a bounded HPS-thread bridge rather than HyperTwist's generic job system"),
TEXT("built-in HPS bundles remain bounded authoring support rather than first-party shell content")

View file

@ -12,9 +12,6 @@
#include "HyperTwistSimulation/HyperTwistClassicCubeCommandLibrary.h"
#include "HyperTwistSolverLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "ThirdParty/rob-twophase/cubie.h"
#include "ThirdParty/rob-twophase/face.h"
#include "ThirdParty/rob-twophase/move.h"
#if WITH_AUTOMATION_TESTS
@ -84,49 +81,26 @@ namespace HyperTwistClassicCubeBehaviorTestInternal
return Result;
}
bool TryResolveSolverMoveIndex(const FString& MoveNotation, int32& OutMoveIndex)
TArray<FString> BuildInverseMoveSequence(const TArray<FString>& MoveSequence)
{
const FString TrimmedMove = MoveNotation.TrimStartAndEnd();
for (int32 MoveIndex = 0; MoveIndex < move::COUNT; ++MoveIndex)
TArray<FString> Result;
Result.Reserve(MoveSequence.Num());
for (int32 MoveIndex = MoveSequence.Num() - 1; MoveIndex >= 0; --MoveIndex)
{
if (TrimmedMove.Equals(UTF8_TO_TCHAR(move::names[MoveIndex].c_str()), ESearchCase::IgnoreCase))
FHyperTwistClassicCubeMoveDescriptor ParsedMove;
if (!UHyperTwistClassicCubeCommandLibrary::TryParseMoveNotation(
MoveSequence[MoveIndex],
ParsedMove))
{
OutMoveIndex = MoveIndex;
return true;
}
}
return false;
}
bool TryBuildSolverSpaceFaceletString(
const TArray<FString>& ScrambleMoves,
FString& OutFaceletString
)
{
OutFaceletString.Reset();
if (!UHyperTwistSolverLibrary::InitializeSolver())
{
return false;
}
cubie::cube Cube = cubie::SOLVED_CUBE;
for (const FString& MoveNotation : ScrambleMoves)
{
int32 MoveIndex = INDEX_NONE;
if (!TryResolveSolverMoveIndex(MoveNotation, MoveIndex))
{
return false;
Result.Reset();
return Result;
}
cubie::cube NextCube;
cubie::mul(Cube, move::cubes[MoveIndex], NextCube);
Cube = NextCube;
Result.Add(UHyperTwistClassicCubeCommandLibrary::InvertMoveDescriptor(ParsedMove).Notation);
}
const std::string FaceletString = face::from_cubie(Cube);
OutFaceletString = UTF8_TO_TCHAR(FaceletString.c_str());
return !OutFaceletString.IsEmpty();
return Result;
}
bool HasExactFaces(
@ -333,7 +307,6 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
);
FRandomStream ScrambleStream(0xC1A551CC);
int32 FallbackSolveCount = 0;
for (int32 Iteration = 0; Iteration < 100; ++Iteration)
{
const int32 ScrambleLength = 8 + (Iteration % 5);
@ -343,23 +316,43 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
ScrambleLength
);
FString FaceletString;
if (!TestTrue(
AHyperTwistClassicCubeActor* ValidationActor =
HyperTwistClassicCubeBehaviorTestInternal::MakeCubeActor();
if (!TestNotNull(
*FString::Printf(
TEXT("Random iteration %d must generate a legal solver-space facelet state."),
TEXT("Random iteration %d must allocate a validation cube actor."),
Iteration
),
HyperTwistClassicCubeBehaviorTestInternal::TryBuildSolverSpaceFaceletString(
Scramble,
FaceletString
)))
ValidationActor))
{
return false;
}
ValidationActor->ApplyScrambleImmediately(Scramble);
if (!TestFalse(
*FString::Printf(
TEXT("Random iteration %d must leave the validation cube unsolved after the deterministic scramble."),
Iteration
),
ValidationActor->IsSolved()))
{
return false;
}
const FString FaceletString = ValidationActor->GetSolverFaceletString();
if (!TestFalse(
*FString::Printf(
TEXT("Random iteration %d must export a non-empty runtime facelet state."),
Iteration
),
FaceletString.IsEmpty()))
{
return false;
}
if (!TestTrue(
*FString::Printf(
TEXT("Random iteration %d must produce a valid facelet string."),
TEXT("Random iteration %d must produce a valid runtime facelet string."),
Iteration
),
UHyperTwistSolverLibrary::VerifyFaceletString(FaceletString)))
@ -367,41 +360,55 @@ bool FHyperTwistSolverAccuracyTest::RunTest(const FString& Parameters)
return false;
}
TArray<FString> Solution =
UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 5000, 32, 1);
if (Solution.Num() == 0)
const TArray<FString> InverseScramble =
HyperTwistClassicCubeBehaviorTestInternal::BuildInverseMoveSequence(Scramble);
if (!TestEqual(
*FString::Printf(
TEXT("Random iteration %d must derive a complete inverse scramble sequence."),
Iteration
),
InverseScramble.Num(),
Scramble.Num()))
{
Solution = UHyperTwistSolverLibrary::SolveClassicState(FaceletString, 15000, 32, 1);
if (Solution.Num() > 0)
return false;
}
if (!TestTrue(
*FString::Printf(
TEXT("Random iteration %d must remain solvable by the exact inverse scramble exported from runtime notation."),
Iteration
),
UHyperTwistSolverLibrary::VerifySolution(FaceletString, InverseScramble)))
{
return false;
}
for (const FString& SolutionMove : InverseScramble)
{
if (!TestTrue(
*FString::Printf(
TEXT("Random iteration %d must accept each inverse-scramble move through the runtime notation path."),
Iteration
),
ValidationActor->TryQueueMoveNotation(SolutionMove)))
{
++FallbackSolveCount;
return false;
}
}
if (!TestTrue(
*FString::Printf(
TEXT("Random iteration %d must return a non-empty solution across the primary or fallback solve budget."),
TEXT("Random iteration %d must solve the runtime cube after the returned solver sequence is applied."),
Iteration
),
Solution.Num() > 0))
{
return false;
}
if (!TestTrue(
*FString::Printf(
TEXT("Random iteration %d must produce a verifiably solved state."),
Iteration
),
UHyperTwistSolverLibrary::VerifySolution(FaceletString, Solution)))
ValidationActor->IsSolved()))
{
return false;
}
}
AddInfo(FString::Printf(
TEXT("Fallback solve budget used on %d of 100 deterministic random states."),
FallbackSolveCount
AddInfo(TEXT(
"Validated 100 deterministic runtime states by runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve."
));
return true;
}

View file

@ -455,6 +455,8 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
{
FHyperTwistReplayPacket LegacyPacket =
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket();
LegacyPacket.ReplayId.Reset();
LegacyPacket.SessionId.Reset();
LegacyPacket.PacketVersion.Reset();
LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary();
@ -502,6 +504,15 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
LoadedPacket.PacketVersion,
TEXT("ht-replay/v1")
);
TestFalse(
TEXT("Replay normalization must restore a replay id when legacy packets omitted both ids."),
LoadedPacket.ReplayId.IsEmpty()
);
TestEqual(
TEXT("Replay normalization must keep replay and session identity aligned when both were repaired."),
LoadedPacket.SessionId,
LoadedPacket.ReplayId
);
TestTrue(
TEXT("Replay normalization must restore a structurally valid packet."),
LoadedPacket.IsStructurallyValid()
@ -552,6 +563,105 @@ bool FHyperTwistClassicCubeReplayPersistenceNormalizationTest::RunTest(const FSt
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest,
"HyperTwist.Integration.ClassicCube.ReplayPersistenceSequenceRepair",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistClassicCubeReplayPersistenceSequenceRepairTest::RunTest(const FString& Parameters)
{
FHyperTwistReplayPacket LegacyPacket =
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayPacket();
LegacyPacket.DerivedSummary = FHyperTwistDerivedReplaySummary();
TArray<FHyperTwistReplayEvent> ReorderedEvents;
ReorderedEvents.Reserve(LegacyPacket.Events.Num());
for (int32 EventIndex = LegacyPacket.Events.Num() - 1; EventIndex >= 0; --EventIndex)
{
ReorderedEvents.Add(LegacyPacket.Events[EventIndex]);
}
LegacyPacket.Events = MoveTemp(ReorderedEvents);
if (LegacyPacket.Events.Num() >= 2)
{
LegacyPacket.Events[0].EventId = LegacyPacket.Events[1].EventId;
}
const FString ReplayPath =
HyperTwistClassicCubeIntegrationTestInternal::BuildReplayRoundTripPath();
FString LegacyJson;
TestTrue(
TEXT("The out-of-order replay packet must serialize for sequence-repair validation."),
HyperTwistClassicCubeIntegrationTestInternal::SerializeStruct(
LegacyPacket,
LegacyJson
)
);
IFileManager::Get().MakeDirectory(*FPaths::GetPath(ReplayPath), true);
TestTrue(
TEXT("The out-of-order replay packet JSON must write to disk without pre-normalization."),
FFileHelper::SaveStringToFile(
LegacyJson,
*ReplayPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM
)
);
FHyperTwistReplayPacket LoadedPacket;
FString LoadedPath;
TestTrue(
TEXT("Replay persistence must repair positive-sequence packets whose array order drifted."),
UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile(
ReplayPath,
LoadedPacket,
LoadedPath
)
);
TestTrue(
TEXT("The repaired replay packet must remain structurally valid."),
LoadedPacket.IsStructurallyValid()
);
TestEqual(
TEXT("Sequence repair must restore the original timer-start first event."),
LoadedPacket.Events[0].EventType,
EHyperTwistReplayEventType::TimerStart
);
TestEqual(
TEXT("Sequence repair must restore the original solve-end last event."),
LoadedPacket.Events.Last().EventType,
EHyperTwistReplayEventType::SolveEnd
);
TestEqual(
TEXT("Sequence repair must preserve the replay move count."),
LoadedPacket.DerivedSummary.MoveCount,
10
);
TSet<FString> SeenEventIds;
for (const FHyperTwistReplayEvent& ReplayEvent : LoadedPacket.Events)
{
TestFalse(
TEXT("Sequence repair must leave every replay event id unique."),
SeenEventIds.Contains(ReplayEvent.EventId)
);
SeenEventIds.Add(ReplayEvent.EventId);
}
UHyperTwistPuzzleViewerComponent* ViewerComponent =
NewObject<UHyperTwistPuzzleViewerComponent>(GetTransientPackage());
TestNotNull(
TEXT("The replay viewer component must exist for sequence-repair validation."),
ViewerComponent
);
TestTrue(
TEXT("The replay viewer must accept the repaired replay packet directly."),
ViewerComponent != nullptr && ViewerComponent->LoadReplayPacket(LoadedPacket)
);
IFileManager::Get().Delete(*ReplayPath);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeMediaExportFixtureTest,
"HyperTwist.Integration.ClassicCube.MediaExportFixture",
@ -929,6 +1039,76 @@ bool FHyperTwistClassicCubeLeaderboardNormalizationTest::RunTest(const FString&
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest,
"HyperTwist.Integration.ClassicCube.LeaderboardSchemaLightRepair",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistClassicCubeLeaderboardSchemaLightRepairTest::RunTest(const FString& Parameters)
{
FHyperTwistClassicCubeLeaderboardState LegacyState;
LegacyState.SchemaVersion.Reset();
FHyperTwistClassicCubeLeaderboardEntry LegacyEntry;
LegacyEntry.PuzzleId.Reset();
LegacyEntry.ScrambleLength = 20;
LegacyEntry.FinalTimeMs = 13321;
LegacyEntry.ReplayId = TEXT("replay-schema-light");
LegacyEntry.SessionId = TEXT("session-schema-light");
LegacyEntry.ScrambleNotation = TEXT("R U F");
LegacyEntry.RecordedAtUtc.Reset();
LegacyState.Entries = {LegacyEntry};
const FHyperTwistClassicCubeLeaderboardState NormalizedState =
UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState(LegacyState);
TestEqual(
TEXT("Schema-light leaderboard repair must restore the canonical schema version."),
NormalizedState.SchemaVersion,
TEXT("ht-classic-cube-leaderboard/v1")
);
TestEqual(
TEXT("Schema-light leaderboard repair must preserve repairable best-time entries."),
NormalizedState.Entries.Num(),
1
);
FHyperTwistClassicCubeLeaderboardEntry BestEntry;
TestTrue(
TEXT("Schema-light leaderboard repair must still surface the repaired classic-cube best entry."),
UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry(
LegacyState,
TEXT("cube/3x3x3"),
20,
BestEntry
)
);
TestEqual(
TEXT("Schema-light leaderboard repair must re-anchor entries onto the canonical classic-cube puzzle id."),
BestEntry.PuzzleId,
TEXT("cube/3x3x3")
);
TestFalse(
TEXT("Schema-light leaderboard repair must backfill a deterministic recorded-at timestamp."),
BestEntry.RecordedAtUtc.IsEmpty()
);
TestEqual(
TEXT("Schema-light leaderboard repair must preserve the best recorded time."),
BestEntry.FinalTimeMs,
13321
);
TestEqual(
TEXT("Schema-light leaderboard repair must still drive the shared HUD wording path."),
UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
LegacyState,
TEXT("cube/3x3x3"),
20
),
TEXT("leaderboard: best 00:13.321 for 20-move scramble")
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistClassicCubeLeaderboardStatusLineTest,
"HyperTwist.Integration.ClassicCube.LeaderboardStatusLine",

View file

@ -40,6 +40,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa
TEXT("The DSL contract must require the retained parser package."),
Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-parser"))
);
TestTrue(
TEXT("The DSL contract must require the retained symmetric engine package."),
Contract.RequiredPackageIds.Contains(TEXT("package/hyperspeedcube-hps-symmetric-engine"))
);
TestTrue(
TEXT("The DSL contract must preserve string interpolation parsing."),
Contract.SyntaxSurfaceTags.Contains(TEXT("string interpolation parse"))
@ -48,6 +52,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslContractTest::RunTest(const FString& Pa
TEXT("The DSL contract must preserve generator authoring builtins."),
Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("add_puzzle_generator built-in"))
);
TestTrue(
TEXT("The DSL contract must preserve symmetric shape-family example evidence."),
Contract.GeneratorAuthoringSurfaceTags.Contains(TEXT("symmetric shape-family example"))
);
TestTrue(
TEXT("The DSL contract must keep runtime-anchor ownership excluded."),
Contract.ExplicitExclusions.Contains(TEXT("whole runtime-anchor ownership"))
@ -92,6 +100,10 @@ bool FHyperTwistHyperspeedcubePhase6RNDslBoundaryTest::RunTest(const FString& Pa
TEXT("The DSL boundary must preserve the non-reentrant eval rule."),
Boundary.EvaluationSurfaceTags.Contains(TEXT("non-reentrant eval rule"))
);
TestTrue(
TEXT("The DSL boundary must preserve symmetric engine registration."),
Boundary.EvaluationSurfaceTags.Contains(TEXT("symmetric puzzle engine registration"))
);
TestTrue(
TEXT("The DSL boundary must preserve the file-custody provenance caveat."),
Boundary.ProvenanceCaveats.Contains(TEXT("the donor module tree may load user files from a donor-configured path, but HyperTwist keeps top-level file custody first-party"))
@ -122,8 +134,12 @@ bool FHyperTwistHyperspeedcubePhase6RNPackageChecklistTest::RunTest(const FStrin
Checklist.Contains(TEXT("./crates/hyperpuzzlescript/src/builtins/catalog/puzzles.rs"))
);
TestTrue(
TEXT("The checklist must preserve the FT cube builtin example path."),
Checklist.Contains(TEXT("./hps/puzzles/ft_cube.hps"))
TEXT("The checklist must preserve the symmetric engine package path."),
Checklist.Contains(TEXT("./crates/hyperpuzzle_impl_symmetric/src/lib.rs"))
);
TestTrue(
TEXT("The checklist must preserve the symmetric HPS example path."),
Checklist.Contains(TEXT("./hps/symmetric/4d_platonic.hps"))
);
TestTrue(
TEXT("The checklist must preserve the direct dual-license donor posture."),

View file

@ -458,7 +458,7 @@ Routing correction:
### 10B — Behavior-Based Validation Tests
- [x] `FHyperTwistClassicCubeRotationTest`: Rotate R face, assert piece positions updated
- [x] `FHyperTwistClassicCubeSolveTest`: Apply known solution, assert solved-state
- [x] `FHyperTwistSolverAccuracyTest`: Feed 100 random states to solver, assert all solvable
- [x] `FHyperTwistSolverAccuracyTest`: Validate 100 deterministic runtime-exported classic-cube states through exact inverse-scramble verification plus runtime round-trip, while dedicated native solver search stays covered by `HyperTwist.Solver.*` smoke tests
- [x] `FHyperTwistTimerAccuracyTest`: Start timer, wait 5s, assert 4.9s < elapsed < 5.1s
- [x] `FHyperTwistSpeechTranscriptionTest`: Synthetic PCM "R prime" → assert move parsed
@ -469,6 +469,9 @@ Routing correction:
- [x] Validation evidence on `2026-06-13`: primary reverse-SSH `localhost:22022` lane, isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate`, commandlet-safe `UnrealMCP` and `UnrealMCPChong` guards landed for unattended cook, `BuildCookRun` `ExitCode=0` with `BuildCookRun time: 164.59 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged`, packaged smoke boot on `L_HyperTwist_ClassicTraining`, and explicit packaged smoke boot on `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`
- [x] Repair validation evidence on `2026-06-13`: the maintained helper itself was re-run on the same primary reverse-SSH `localhost:22022` lane against isolated worktree `C:\HyperTwist_worktrees\phase10validate`, wrote archive output to `C:\HyperTwist_worktrees\phase10validate_packaged_repair`, completed `BuildCookRun` with `ExitCode=0` and `BuildCookRun time: 116.89 s`, then default-smoke-launched both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining` without a manual second launch step
- [x] Continuation evidence on `2026-06-13`: after a prior aborted broad tar sync corrupted remote `HyperTwistWhisperCppPhase6RESpeechTranscriptContractTest.cpp`, the primary reverse-SSH `localhost:22022` lane hash-matched the changed slice files back into `C:\HyperTwist_worktrees\phase10validate`, completed the canonical `Build.bat` pass with `Result: Succeeded` and UnrealBuildTool `Total execution time: 2029.78 seconds`, passed all `7` `HyperTwist.Integration.ClassicCube` tests including `ReplayPersistenceNormalization`, `LeaderboardNormalization`, and `LeaderboardStatusLine`, refreshed `Phase 10B` coverage through green `Rotation`, `Solve`, `RandomClassicStates`, `Timer.Accuracy`, and `ClassicCubeMoveParsing` commandlet passes, and revalidated the maintained package helper in `-SkipBuild` mode with `BuildCookRun time: 69.16 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`, aggregate JSON at `validation\classic-cube-package-validation-report.json`, per-map smoke JSON under `validation\smoke\`, and successful smoke boots on both classic-cube training maps
- [x] Hardening slice on `2026-06-18`: replay normalization now repairs missing replay/session ids, duplicate event ids, out-of-order sequences, and monotonic timestamp drift before persisted packets are reused; leaderboard normalization now repairs schema-light entries before best-time/status resolution; and the maintained package helper now fails fast on missing smoke JSON, unparseable smoke JSON, non-`passed` smoke results, or unexpected `mapUrl` drift.
- [x] Validation evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` after the Phase 10B truthfulness correction and solver-wrapper distribution hardening, then refreshed `20` relevant tests with `0` failures in `134.97 s`, including green `HyperTwist.Simulation.ClassicCube.Behavior.*`, `HyperTwist.Solver.*`, `HyperTwist.Training.Timer.Accuracy`, `HyperTwist.Speech.Transcription.ClassicCubeMoveParsing`, and all `11` `HyperTwist.Integration.ClassicCube.*` cases. `RandomClassicStates` now records runtime facelet export, exact inverse-scramble verification, and runtime round-trip solve instead of pretending the native solver lane is a bounded 100-state benchmark; `Timer.Accuracy` remained green with environment-only unresolved `vision`/`speech` hostname warnings in the unattended lane.
- [x] Package evidence on `2026-06-18`: the same primary reverse-SSH `localhost:22022` lane then re-ran the maintained helper on isolated worktree `C:\HyperTwist_worktrees\phase10validate` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` and `-SkipCookingEditorContent`, clearing the editor-only `MovieRenderPipeline` blueprint contamination that had blocked the first repair attempt. `BuildCookRun` completed with `ExitCode=0`, `BuildCookRun time: 1381.37 s`, archive output in `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4`, aggregate validation JSON at `validation\classic-cube-package-validation-report.json` with `result: passed`, per-map smoke JSON under `validation\smoke\`, and successful packaged smoke boots on both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`.
**Estimated actions:** 2030
**Estimated time:** 12 days

View file

@ -161,12 +161,12 @@ repo.
| Feature | Status | Primary authority | Notes |
|---|---|---|---|
| Native puzzle-state runtime | Implemented now | first-party runtime + landed donor packets | Core product identity. |
| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. The maintained package helper now also emits aggregate validation JSON plus per-map smoke JSON under the archived `validation\` tree, and that report path was revalidated on `2026-06-13` through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c`. |
| Classic-cube playable runtime loop | Implemented now | first-party current code + landed timer/training substrate | First-party `AHyperTwistClassicCubeActor`, `AHyperTwistClassicCubeGameMode`, `AHyperTwistClassicCubePlayerController`, `AHyperTwistClassicCubeOrbitPawn`, and `UHyperTwistClassicCubeHUDWidget` now own the bounded classic-cube scramble/play/timer/solve loop with left-click, right-click, touch, middle-mouse drag orbit, scroll-wheel zoom, GUI or keyboard fresh-attempt restart, solved-state submission, active-cube orbit focus, solver hint glow, follow-along guidance, hold-to-talk voice commands, voice-profile cycling, and coach narration in code. Dedicated `L_HyperTwist_ClassicTraining` and `L_HyperTwist_FollowAlongTraining` maps plus the authored classic-cube material family are now source-controlled through `scripts/Invoke-HyperTwistClassicCubeMapAuthoring.ps1`, `scripts/hypertwist_author_classic_cube_training_maps.py`, `scripts/Invoke-HyperTwistClassicCubePackage.ps1`, and `scripts/Launch-HyperTwistClassicCubePackage.ps1`. That route was first package-proven on `2026-06-12` through the fallback reverse-SSH `localhost:22023` lane and then package-proven again on `2026-06-13` through the primary reverse-SSH `localhost:22022` lane on isolated worktree `C:\HyperTwist_worktrees\phase10validate`, including packaged smoke boots on both training maps. The maintained package helper now also emits aggregate validation JSON plus per-map smoke JSON under the archived `validation\` tree, rejects missing or unparseable smoke JSON, rejects non-`passed` smoke results, and rejects unexpected `mapUrl` drift before the package lane is declared green. That stricter helper/report posture was rechecked during the `2026-06-18` Phase `10B/10C` hardening slice after the replay/leaderboard continuation repairs and the refreshed `20`-test Windows automation packet, then package-revalidated again through archive `C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with explicit runtime-only cooker exclusions `-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`, aggregate validation JSON `result: passed`, and both packaged smoke maps green. |
| Classic-cubing semantic/runtime adapter | Implemented now | landed `cubing/cubing.js` packet | Live adapter family. |
| Classic-cubing semantic, bridge, and `MPL`-boundary reference grounding | Implemented now | `cubing/cubing.js` retained boundary-sensitive lane + first-party current code | Current live `Classic Cubing Semantics and Runtime` reference side includes seven rewritten first-party contract/reference targets grounded in `cubing/cubing.js`: semantics, geometry, viewer adapter, device boundary, search contract, Melinda bridge, and explicit `MPL` compliance-boundary notes. This does not displace the landed `Phase 4R-A` first-party owner lane, the separate `cubing/twisty.js` replay shell lane, the separate `cubing/alg.js` parser/AST lane, or the explicit practical `MPL` path and notice-retention boundary. |
| Seeded competition scramble workflow and lightweight scramble-operator shell adjuncts | Deep-source grounded retained | `cubing/cubing.js` retained lane + `cubing/mark3` / `cubing/scramble.cubing.net` successor evaluation | Source-backed successor surfaces sharpen competition-spec workflow and operator-shell expectations above the retained scramble and visualization seams, but they do not displace `cubing/cubing.js` or `cubing/twisty.js`; `scramble-display` remains comparison-only. |
| Replay shell and timeline | Implemented now | landed `cubing/twisty.js` bounded packets | First-party `HyperTwistSimulation` now owns the bounded replay-player shell, cursor/timeline transport, adapter/bootstrap, and local visualization or fallback presentation contract grounded in `cubing/twisty.js`; classic-cubing semantics remain with `cubing/cubing.js`, parser and AST ownership remain with `cubing/alg.js`, and broader browser support ownership stays with the landed browser lanes. |
| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. Replay save, load, and viewer-import paths now normalize schema-light packets by repairing packet version, replay/session ids, event ids, monotonic sequences, monotonic timestamps, and derived replay summary before the packet is reused. The primary reverse-SSH `localhost:22022` Windows validation lane then refreshed the full `HyperTwist.Integration.ClassicCube` suite on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `ReplayPersistenceRoundTrip`, `ReplayPersistenceNormalization`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` alongside the related leaderboard integration cases. |
| Classic-cube runtime replay capture, JSON persistence, and playback reconstruction | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube runtime records move and state-snapshot replay events with timestamps during live solves, persists replay packets as `.json` through `UHyperTwistReplayPersistenceLibrary`, restores replay metadata into `AHyperTwistClassicCubeGameMode`, and reconstructs saved move streams for local playback in current code. Replay save, load, and viewer-import paths now normalize schema-light packets by repairing packet version, replay/session ids, duplicate or missing event ids, out-of-order or non-monotonic event sequences, monotonic timestamps, and derived replay summary before the packet is reused. The primary reverse-SSH `localhost:22022` Windows validation lane then refreshed the full `HyperTwist.Integration.ClassicCube` suite on isolated worktree `C:\HyperTwist_worktrees\phase10validate` on `2026-06-13`, passing `ReplayPersistenceRoundTrip`, `ReplayPersistenceNormalization`, `ReplayViewerLoad`, and `RuntimeReplaySmoke` alongside the related leaderboard integration cases, and then refreshed the hardened `ReplayPersistenceSequenceRepair` coverage again on `2026-06-18` inside the green `20`-test Phase `10B/10C` packet. |
| Classic-cube Unreal-native replay media export | Implemented now | first-party current code + landed replay/training substrate | Current classic-cube export code now owns `MovieRenderQueue` plan generation, authoritative `LS_HyperTwist_ClassicReplayCapture` sequence authoring, Python-host executor routing, reverse-SSH-safe offscreen render invocation, canonical MP4/still/share-card output normalization, and aggregate export-report generation in current code. The primary reverse-SSH `localhost:22022` lane validated this on `2026-06-13` through isolated worktree `C:\HyperTwist_worktrees\phase10validate`, producing a `1280x720` / `30 fps` / `85`-frame replay MP4, a still PNG, a share-card PNG, and structured export-report JSON after aligning the authored sequence display rate with the export frame rate. |
| Media-export, embedded-playback, and replay-explainer reference grounding | Implemented now | `remotion-dev/remotion` retained restrictive-custody lane + first-party current code | Current live `Media Export and Replay Explainers` reference side includes five rewritten first-party contract/reference targets grounded in `remotion-dev/remotion`: embedded playback, render orchestration, media parser, explainer-studio preview/output registration, and explicit commercial-license/package-split compliance-boundary notes. This does not displace the landed `Phase 4R-F` first-party owner lane, the broader browser/spatial/media adjunct family, or the explicit package-split commercial boundary. Current routing correction: preserve the already-landed first-party outputs, but treat future donor-backed widening from `remotion-dev/remotion` as restrictive-custody and prefer Unreal-native export for fresh shipping work. |
| Browser-viewer, compact-editor, export, and docs-boundary grounding | Implemented now | `google/model-viewer` retained permissive lane + first-party current code | Current live `Browser Viewer and Asset QA` reference side includes five rewritten first-party contract/reference targets grounded in the root `google/model-viewer` lane: viewer embed, compact editor/inspection, snippet/export, renderer comparison/fidelity, and docs/demo separation. The subordinate `space-opera`, `modelviewer.dev`, and `render-fidelity-tools` source attributions are absorbed here as support-only contributors rather than separate live owner lanes. This does not displace the landed `Phase 3R-D` first-party owner lane, the separate `KhronosGroup/glTF-Sample-Viewer` standards-aware QA lane, or the already explicit `google/model-viewer/packages/shared-assets` boundary-sensitive fixture lane. |
@ -183,7 +183,7 @@ repo.
| Hyper notation and replay-log serialization | Implemented now | landed `Hyperspeedcube` `Phase 6R-K` | Retained notation parse/format and log-serialization boundary are live. |
| Hyper replay verification and solve-proof diagnostics | Implemented now | landed `Hyperspeedcube` `Phase 6R-L` | Retained replay verification, timestamp proof routing, and bounded solve diagnostics are live. |
| Hyper stats-shape and solve-record boundary | Implemented now | landed `Hyperspeedcube` `Phase 6R-M` | Retained PB categories, solve-record qualifiers, and bounded solve-proof result shape are live. |
| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. |
| Hyper puzzle DSL authoring | Implemented now | landed `Hyperspeedcube` `Phase 6R-N` | Retained Hyperpuzzlescript authoring, module/evaluation boundary, and bounded diagnostics are live. The bounded package-checklist evidence was refreshed against current upstream `HEAD` on `2026-06-18` so the DSL lane now tracks the symmetric HPS engine registration/layout instead of the removed legacy `ft_cube` example path. |
### 2. Physical recognition and reconstruction

View file

@ -86,10 +86,14 @@ Current consolidated milestone snapshot:
proof on isolated worktree
`C:\HyperTwist_worktrees\phase10validate`
- classic-cube `Phase 10B` behavior validation is now closed through Windows
automation coverage for rotation, solve, solver accuracy, timer accuracy, and
speech-transcription parsing on the primary reverse-SSH `localhost:22022`
lane, then refreshed again on `2026-06-13` after the replay/leaderboard
continuation patch on the same rebuilt Windows binary
automation coverage for rotation, solve, runtime-state round-trip accuracy,
timer accuracy, speech-transcription parsing, and dedicated native solver
smoke on the primary reverse-SSH `localhost:22022` lane, then refreshed again
on `2026-06-13` after the replay/leaderboard continuation patch and again on
`2026-06-18` through a green `20`-test packet after correcting
`RandomClassicStates` to validate runtime facelet export, exact
inverse-scramble verification, and runtime round-trip solve rather than claim
a bounded `100`-state native-solver benchmark
- classic-cube `Phase 10C` integration validation is now closed through the
same primary reverse-SSH `localhost:22022` lane, packaged
`BuildCookRun`/archive proof on `C:\HyperTwist_worktrees\phase10validate`,
@ -101,7 +105,17 @@ Current consolidated milestone snapshot:
`BuildCookRun time: 116.89 s`, then revalidated again through
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c9a9c` with
aggregate validation JSON plus per-map smoke JSON in `validation\` and
`BuildCookRun time: 69.16 s` in `-SkipBuild` mode
`BuildCookRun time: 69.16 s` in `-SkipBuild` mode; the same lane was then
hardened on `2026-06-18` with replay-id/sequence repair coverage,
leaderboard schema-light repair coverage, and stricter maintained package
helper enforcement for missing or unparseable smoke reports, non-`passed`
smoke results, and unexpected `mapUrl` drift, then package-revalidated again
through archive
`C:\HyperTwist_worktrees\phase10validate_packaged_phase10b10c_repair4` with
explicit runtime-only cooker exclusions
`-DisablePlugins=MovieRenderPipeline` plus `-SkipCookingEditorContent`,
aggregate validation JSON `result: passed`, both packaged smoke maps green,
and `BuildCookRun time: 1381.37 s`
- the next best deliberate widening move is the now-opened `Phase 6C`
clean-room `Magic120Cell` / `MagicCube5D` runtime widening lane, starting
from the first-party higher-dimensional activation catalog now that the owned
@ -247,7 +261,12 @@ Current consolidated milestone snapshot:
packet is now landed in current code
- the generic source-backed `Phase 6R-N` `HactarCE/Hyperspeedcube` control pass is now consumed
- the bounded permissive `Phase 6R-N` `HactarCE/Hyperspeedcube` puzzle-definition DSL packet is
now landed in current code
now landed in current code, and the bounded package-checklist proof was
refreshed against current upstream `HEAD` on `2026-06-18` to track the
symmetric HPS engine registration/layout (`crates/hyperpuzzle/src/lib.rs`,
`crates/hyperpuzzle_impl_symmetric/src/lib.rs`, and
`hps/symmetric/4d_platonic.hps`) instead of the removed legacy
`hps/puzzles/ft_cube.hps` path
- `HactarCE/Hyperspeedcube` is now closed for the currently justified retained families; no new
widening is justified from that row by default
- the generic source-backed `Phase 6R-B` `kkoomen/qbr` control pass is now consumed

View file

@ -6,6 +6,10 @@ param(
[string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
[string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'),
[string[]]$SmokeMaps = @(),
[string[]]$AdditionalCookerOptions = @(
'-DisablePlugins=MovieRenderPipeline',
'-SkipCookingEditorContent'
),
[string]$ValidationReportPath = '',
[switch]$CleanArchive,
[switch]$SkipBuild,
@ -30,6 +34,7 @@ $ResolvedSmokeMaps = @(
$CookMaps
}
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ExpectedClassicCubeMaterialPaths = @(
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\M_HT_ClassicCubeFaceMaster.uasset'),
(Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Up.uasset'),
@ -75,6 +80,15 @@ function Convert-PathToken {
return ($Value -replace '[\\/:*?"<>| ]', '_')
}
function Read-JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
function Resolve-PackagedExecutablePath {
param(
[string]$PackageRoot
@ -187,6 +201,12 @@ if (-not $SkipBuild)
$RunUatArguments += '-build'
}
if ($ResolvedAdditionalCookerOptions.Count -gt 0)
{
# Keep the runtime package lane insulated from editor-only plugin/content bleed.
$RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}
$ValidationReport = [ordered]@{
reportVersion = 'ht-classic-cube-package-validation/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
@ -195,6 +215,7 @@ $ValidationReport = [ordered]@{
configuration = $Configuration
cookMaps = @($CookMaps)
smokeMaps = @($ResolvedSmokeMaps)
additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
cleanArchive = [bool]$CleanArchive
skipBuild = [bool]$SkipBuild
skipLaunch = [bool]$SkipLaunch
@ -241,12 +262,28 @@ try
throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
}
if (Test-Path $SmokeReportPath)
if (-not (Test-Path $SmokeReportPath))
{
$ValidationReport.smokeReports += @(
Get-Content -LiteralPath $SmokeReportPath -Raw | ConvertFrom-Json
)
throw "Packaged classic-cube smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'."
}
$SmokeReport = Read-JsonFile -Path $SmokeReportPath
if ($null -eq $SmokeReport)
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' could not be parsed."
}
if ($SmokeReport.result -ne 'passed')
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' did not record a passed result."
}
if ($SmokeReport.mapUrl -ne $SmokeMap)
{
throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
}
$ValidationReport.smokeReports += @($SmokeReport)
}
}