diff --git a/Directory.Build.props b/Directory.Build.props index c8a3c67..ac7209e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,8 +2,8 @@ $(CULTLIB_ROOT) $(MSBuildThisFileDirectory)..\CultLib - - 42dce3611a81adc5c5fac54cadbaa5637d6022d3 + + abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938 C:\Program Files\Git\cmd\git.exe git diff --git a/README.md b/README.md index ee44b52..da440b3 100644 --- a/README.md +++ b/README.md @@ -156,11 +156,11 @@ handoff state lives in [`state/spine.yaml`](state/spine.yaml). ## Building against CultLib -Builds are pinned to CultLib 2.0.0 (tag `cultlib-unity-v2.0.0`, revision `42dce3611a81adc5c5fac54cadbaa5637d6022d3`); the build fails if the CultLib checkout differs or is dirty. Create a pinned worktree and point the build at it: +Builds are pinned to CultLib revision `abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938` (#91); the build fails if the CultLib checkout differs or is dirty. Create a pinned worktree and point the build at it: ```powershell -git -C worktree add --detach cultlib-unity-v2.0.0 +git -C worktree add --detach abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938 dotnet build -p:CultLibRoot= # or set CULTLIB_ROOT= ``` -Without an override the sibling `..\CultLib` checkout is used and must itself be at that tag. +Without an override the sibling `..\CultLib` checkout is used and must itself be at that revision. diff --git a/src/AquaSynth.Core/CultCachePatchDocument.cs b/src/AquaSynth.Core/CultCachePatchDocument.cs index 9e73596..12e9abe 100644 --- a/src/AquaSynth.Core/CultCachePatchDocument.cs +++ b/src/AquaSynth.Core/CultCachePatchDocument.cs @@ -27,7 +27,8 @@ public sealed record CultCachePatchDatabaseEntry( [property: Key(1)] string Family, [property: Key(2)] string Name, [property: Key(3)] string Source, - [property: Key(4)] string Notes); + [property: Key(4)] string Notes, + [property: Key(5), CultElementId("Path")] string ElementId = ""); [MessagePackObject] public sealed record CultCachePatchClaimCard( @@ -36,7 +37,8 @@ public sealed record CultCachePatchClaimCard( [property: Key(2)] CultCacheReferenceTruth Reference, [property: Key(3)] CultCacheIntentTruth Intent, [property: Key(4)] string Tier, - [property: Key(5)] CultCacheProofTruth Proof); + [property: Key(5)] CultCacheProofTruth Proof, + [property: Key(6), CultElementId("Id")] string ElementId = ""); [MessagePackObject] public sealed record CultCacheSpeechClaimCard( @@ -45,7 +47,8 @@ public sealed record CultCacheSpeechClaimCard( [property: Key(2)] CultCacheSpeechTargetTruth Target, [property: Key(3)] CultCacheIntentTruth Intent, [property: Key(4)] string Tier, - [property: Key(5)] CultCacheProofTruth Proof); + [property: Key(5)] CultCacheProofTruth Proof, + [property: Key(6), CultElementId("Id")] string ElementId = ""); [MessagePackObject] public sealed record CultCacheReferenceTruth( diff --git a/src/AquaSynth.Core/IpaTrialResults.cs b/src/AquaSynth.Core/IpaTrialResults.cs index 7957cc9..589f2c4 100644 --- a/src/AquaSynth.Core/IpaTrialResults.cs +++ b/src/AquaSynth.Core/IpaTrialResults.cs @@ -40,7 +40,8 @@ public sealed record PrimitiveTimelineFact( [property: Key(4)] string Unit, [property: Key(5)] int BlockStart, [property: Key(6)] int BlockEnd, - [property: Key(7)] string Summary); + [property: Key(7)] string Summary, + [property: Key(8), CultElementId] string ElementId = ""); [CultDocument("aquasynth.song_challenge_evidence", "aquasynth.song_challenge_evidence.v1")] [MessagePackObject] diff --git a/src/AquaSynth.Core/SpeechDistributedTraining.cs b/src/AquaSynth.Core/SpeechDistributedTraining.cs index 4b8743a..2c2f62d 100644 --- a/src/AquaSynth.Core/SpeechDistributedTraining.cs +++ b/src/AquaSynth.Core/SpeechDistributedTraining.cs @@ -67,7 +67,8 @@ public sealed record SpeechTimingReceipt( [property: Key(2)] double LatencyMilliseconds, [property: Key(3)] double BudgetMilliseconds, [property: Key(4)] float Confidence, - [property: Key(5)] string Notes); + [property: Key(5)] string Notes, + [property: Key(6), CultElementId] string ElementId = ""); [MessagePackObject] public sealed record SpeechUtteranceEmbeddingInputSnapshot( @@ -107,13 +108,15 @@ public sealed record SpeechPhoneticEventSnapshot( public sealed record SpeechScoreMetric( [property: Key(0)] string Name, [property: Key(1)] float Value, - [property: Key(2)] float Weight); + [property: Key(2)] float Weight, + [property: Key(3), CultElementId] string ElementId = ""); [MessagePackObject] public sealed record SpeechRenderArtifact( [property: Key(0)] string Kind, [property: Key(1)] string Uri, - [property: Key(2)] string ContentHash); + [property: Key(2)] string ContentHash, + [property: Key(3), CultElementId] string ElementId = ""); public enum SpeechRenderStatus { @@ -141,7 +144,8 @@ public sealed record SpeechWorkerPayloadArtifact( [property: Key(0)] string Kind, [property: Key(1)] string Uri, [property: Key(2)] string ContentHash, - [property: Key(3)] string MediaType); + [property: Key(3)] string MediaType, + [property: Key(4), CultElementId] string ElementId = ""); [CultDocument("aquasynth.speech_worker_admission_receipt", "aquasynth.speech_worker_admission_receipt.v1")] [MessagePackObject] diff --git a/src/AquaSynth.Faust/AquaSynthDaemonService.cs b/src/AquaSynth.Faust/AquaSynthDaemonService.cs index a01135b..907b518 100644 --- a/src/AquaSynth.Faust/AquaSynthDaemonService.cs +++ b/src/AquaSynth.Faust/AquaSynthDaemonService.cs @@ -77,7 +77,8 @@ public sealed record AquaSynthAutomationStreamCommand( [MessagePackObject] public sealed record AquaSynthAutomationControlFrame( [property: Key(0)] int Block, - [property: Key(1)] Dictionary Controls); + [property: Key(1)] Dictionary Controls, + [property: Key(2), CultElementId] string ElementId = ""); [CultDocument("aquasynth.command.instrument_open", AquaSynthDaemonSchemas.InstrumentOpenCommand)] [MessagePackObject] @@ -276,7 +277,8 @@ public sealed record AquaSynthCultMeshStreamDescriptor( [property: Key(6)] int Channels, [property: Key(7)] int FramesPerPacket, [property: Key(8)] string[] PreferredTransports, - [property: Key(9)] string? MetadataSchemaId); + [property: Key(9)] string? MetadataSchemaId, + [property: Key(10), CultElementId("StreamId")] string ElementId = ""); [MessagePackObject] public sealed record AquaSynthAutomationPacketReceipt( @@ -289,7 +291,8 @@ public sealed record AquaSynthAutomationPacketReceipt( [property: Key(6)] string Float32Uri, [property: Key(7)] string ContentHash, [property: Key(8)] string PageRef, - [property: Key(9)] int ControlCount); + [property: Key(9)] int ControlCount, + [property: Key(10), CultElementId("Sequence")] string ElementId = ""); [CultDocument("aquasynth.operator_state", AquaSynthDaemonSchemas.OperatorState)] [CultGlobal] diff --git a/tests/AquaSynth.Dsl.Tests/CultCachePatchDocumentTests.cs b/tests/AquaSynth.Dsl.Tests/CultCachePatchDocumentTests.cs index 2f55645..1c27b12 100644 --- a/tests/AquaSynth.Dsl.Tests/CultCachePatchDocumentTests.cs +++ b/tests/AquaSynth.Dsl.Tests/CultCachePatchDocumentTests.cs @@ -83,7 +83,8 @@ public sealed class CultCachePatchDocumentTests Path.Combine(RepositoryRoot(), "patches", "aquasynth-patch-cultcache.cc")); var fromCatalog = CultCachePatchDocumentCatalog.CreateDefault(); - Assert.Equal(fromCatalog.PatchDatabase.Entries, fromFile.PatchDatabase.Entries); + // The checked-in file predates element ids; a load mints them (derived from Path), so they are not part of the catalog's content. + Assert.Equal(fromCatalog.PatchDatabase.Entries, fromFile.PatchDatabase.Entries.Select(entry => entry with { ElementId = "" })); Assert.Equal(fromCatalog.PatchClaims.Select(claim => claim.Id), fromFile.PatchClaims.Select(claim => claim.Id)); Assert.Equal(fromCatalog.SpeechClaims.Select(claim => claim.Id), fromFile.SpeechClaims.Select(claim => claim.Id)); } diff --git a/tests/AquaSynth.Dsl.Tests/ElementIdTests.cs b/tests/AquaSynth.Dsl.Tests/ElementIdTests.cs new file mode 100644 index 0000000..dd90718 --- /dev/null +++ b/tests/AquaSynth.Dsl.Tests/ElementIdTests.cs @@ -0,0 +1,103 @@ +using AquaSynth.Dsl; +using GameCult.Caching; +using GameCult.Caching.MessagePack; + +namespace AquaSynth.Dsl.Tests; + +// CultLib abd1a92e refuses to register a document whose list element type has no [CultElementId] member (CultCache.cs:1008-1010); +// opening any cache here registers every AquaSynth document type, so these tests also fail if one element type loses its id. +public sealed class ElementIdTests +{ + // Fixtures/CultCache/pre-element-ids.cc was written by the previous pin (CultLib 42dce361), before list elements had ids: + // an automation-stream command with 2 control frames and a stream receipt with 2 stream descriptors and 2 packets. + [Fact] + public async Task AStoreWrittenBeforeElementIdsReopensWithStableUniqueIds() + { + var path = CopyFixture(); + try + { + string[] first; + using (var cache = await CultCacheMessagePack.OpenAsync(path)) + { + var command = cache.GetAll().Single(); + var receipt = cache.GetAll().Single(); + Assert.Equal([0, 4], command.ControlFrames!.Select(frame => frame.Block)); + Assert.Equal(220f, command.ControlFrames![0].Controls["/freq"]); + Assert.Equal(["audio-1", "control-1"], receipt.Streams.Select(stream => stream.StreamId)); + Assert.Equal([0UL, 1UL], receipt.Packets.Select(packet => packet.Sequence)); + first = Ids(command, receipt); + Assert.All(first, id => Assert.False(string.IsNullOrEmpty(id))); + Assert.Equal(2, command.ControlFrames!.Select(frame => frame.ElementId).Distinct().Count()); + Assert.Equal(2, receipt.Packets.Select(packet => packet.ElementId).Distinct().Count()); + } + + // A second reader of the same bytes mints the same ids (load mints from the record key and element path). + using var again = await CultCacheMessagePack.OpenAsync(path); + Assert.Equal(first, Ids(again.GetAll().Single(), again.GetAll().Single())); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public async Task IdsSurviveAWriteAndReload() + { + var path = CopyFixture(); + try + { + string[] first; + using (var cache = await CultCacheMessagePack.OpenAsync(path)) + { + first = Ids(cache.GetAll().Single(), cache.GetAll().Single()); + // An unrelated write rewrites the store in the current shape. + await cache.UpsertAsync(new AquaSynthInstrumentCloseCommand("c-1", "s-1"), new CultRecordHandle(new CultRecordKey("c-1"))); + await cache.FlushAsync(); + } + + using var reloaded = await CultCacheMessagePack.OpenAsync(path); + Assert.Equal(first, Ids(reloaded.GetAll().Single(), reloaded.GetAll().Single())); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public async Task NewWritesKeepNaturalIdsAndDeriveThePacketIdFromItsSequence() + { + var path = Path.Combine(Path.GetTempPath(), $"aquasynth-elementid-{Guid.NewGuid():N}.cc"); + try + { + using (var cache = await CultCacheMessagePack.OpenAsync(path)) + { + var packets = new[] { new AquaSynthAutomationPacketReceipt(7, 0, 1, 128, 0.5f, 0.1f, "u", "h", "p", 0), new AquaSynthAutomationPacketReceipt(9, 1, 1, 128, 0.5f, 0.1f, "u", "h", "p", 0) }; + var streams = new[] { new AquaSynthCultMeshStreamDescriptor("audio-9", "v", "p", "audio", "f32", 48000, 1, 128, [], null) }; + await cache.UpsertAsync(new AquaSynthAutomationStreamReceipt("run", "cmd", "patch", "session", "succeeded", "t", "v", "a", "c", 48000, 128, 1, streams, packets), + new CultRecordHandle(new CultRecordKey("run"))); + await cache.FlushAsync(); + } + + using var reloaded = await CultCacheMessagePack.OpenAsync(path); + var receipt = reloaded.GetAll().Single(); + Assert.Equal(["7", "9"], receipt.Packets.Select(packet => packet.ElementId)); + Assert.Equal("audio-9", receipt.Streams.Single().StreamId); + } + finally + { + File.Delete(path); + } + } + + private static string[] Ids(AquaSynthAutomationStreamCommand command, AquaSynthAutomationStreamReceipt receipt) => + [.. command.ControlFrames!.Select(frame => frame.ElementId), .. receipt.Packets.Select(packet => packet.ElementId)]; + + private static string CopyFixture() + { + var path = Path.Combine(Path.GetTempPath(), $"aquasynth-preids-{Guid.NewGuid():N}.cc"); + File.Copy(Path.Combine(AppContext.BaseDirectory, "Fixtures", "CultCache", "pre-element-ids.cc"), path); + return path; + } +} diff --git a/tests/AquaSynth.Dsl.Tests/Fixtures/CultCache/pre-element-ids.cc b/tests/AquaSynth.Dsl.Tests/Fixtures/CultCache/pre-element-ids.cc new file mode 100644 index 0000000..6bc8bc8 Binary files /dev/null and b/tests/AquaSynth.Dsl.Tests/Fixtures/CultCache/pre-element-ids.cc differ