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