GameCult.Thing.Contract: the name set (tests/thing-contract-parity) and NameSetTests

catalog-members.tsv (27 rows) and type-names.txt (168 types) are generated from the C# types on
Yggdrasil at CultLib b3bd8dd5 and read by NameSetTests; the package owns the names, every other
runtime is held to them. NameSetTests also patches System.Int64 to System.Int32 in a store C#
wrote and requires reopen to refuse. Presence and list tests no longer count ThingDomainException
as a door.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 02:27:31 +02:00
commit 9586392d14
7 changed files with 286 additions and 2 deletions

View file

@ -35,7 +35,7 @@ internal static class Doors
public static IReadOnlyList<Door> All()
{
var doors = new List<Door>();
foreach (var type in Package.GetExportedTypes().Where(t => !t.IsAbstract || t.IsSealed))
foreach (var type in Package.GetExportedTypes().Where(t => (!t.IsAbstract || t.IsSealed) && t != typeof(ThingDomainException)))
{
foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance))
doors.Add(new Door($"new {type.Name}", ctor));

View file

@ -0,0 +1,77 @@
using GameCult.Caching;
using GameCult.Caching.MessagePack;
namespace GameCult.Thing.Contract.Tests;
/// <summary>
/// The names a CultCache store records for the package's documents. CultLib's C# reference compares catalog type names
/// when a store reopens, so these names are the C# package's to own: tests/thing-contract-parity holds them for every
/// runtime package, and they change only in the commit that changes the C# types.
/// </summary>
public sealed class NameSetTests
{
private static string ParityPath(string file) => Path.Combine(AppContext.BaseDirectory, "parity", file);
private static string[] Lines(string file) => File.ReadAllLines(ParityPath(file)).Where(l => l.Length > 0).ToArray();
[Test]
public void TheCatalogEntriesAreTheMembersFile()
{
var actual = Wire.DocumentTypes.OrderBy(t => Wire.Registry.GetRequired(t).ToCatalogEntry().SchemaId, StringComparer.Ordinal)
.SelectMany(t =>
{
var entry = Wire.Registry.GetRequired(t).ToCatalogEntry();
return entry.Members.Select(m => $"{entry.SchemaId}\t{m.Slot}\t{m.MemberName}\t{m.TypeName}");
}).ToArray();
Assert.That(actual, Is.EqualTo(Lines("catalog-members.tsv")));
Assert.That(actual, Has.Length.GreaterThan(20));
}
[Test]
public void ThePublicTypesAreTheTypeNamesFile()
{
var actual = typeof(CommandResult).Assembly.GetExportedTypes().Select(t => t.FullName!).OrderBy(n => n, StringComparer.Ordinal).ToArray();
Assert.That(actual, Is.EqualTo(Lines("type-names.txt")));
Assert.That(actual, Has.Length.GreaterThan(150));
}
[Test]
public void AStoreWhoseRecordedTypeNameChangedIsRefusedOnReopen()
{
var scratch = Path.Combine(Path.GetTempPath(), $"thing-contract-names-{Guid.NewGuid():N}.cc");
try
{
using (var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry }))
{
Assert.That(cache.Commit(batch => batch.Upsert(Builders.FullAssetCatalog())), Is.True);
cache.FlushAsync().GetAwaiter().GetResult();
}
using (var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry, ReadOnly = true }))
Assert.That(cache.GetAll<AssetCatalog>().Single().Assets, Has.Count.EqualTo(2), "the unpatched store reopens");
var bytes = File.ReadAllBytes(scratch);
var from = "System.Int64"u8.ToArray();
var to = "System.Int32"u8.ToArray();
var patched = 0;
for (var i = 0; i + from.Length <= bytes.Length; i++)
{
if (!bytes.AsSpan(i, from.Length).SequenceEqual(from)) continue;
to.CopyTo(bytes, i);
patched++;
}
Assert.That(patched, Is.GreaterThan(0), "the store records the type name");
File.WriteAllBytes(scratch, bytes);
var error = Assert.Catch(() =>
{
using var cache = CultCacheMessagePack.Create(scratch, new CultCacheOpenOptions { Registry = Wire.Registry, ReadOnly = true });
})!;
TestContext.Progress.WriteLine($"NAMEPATCH patched={patched} refused-by: {Wire.Flatten(error)}");
}
finally
{
File.Delete(scratch);
}
}
}

View file

@ -75,5 +75,7 @@ public sealed class PresenceTests
Assert.That(Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(null, "Captures"))!.Domain, Is.EqualTo("Captures"));
var nilItem = Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(new List<string> { "a", null! }, "Captures"))!;
Assert.That((nilItem.Domain, nilItem.Bound), Is.EqualTo(("Captures", "free of nil items")));
var withCanary = Assert.Throws<ThingDomainException>(() => ThingDomain.Items<string>(new List<string> { Wire.Canary, null! }, "Captures"))!;
Assert.That(withCanary.Message, Does.Not.Contain(Wire.Canary));
}
}

View file

@ -34,7 +34,7 @@ public sealed class ReadOnlyListTests
}
Assert.That(offenders, Is.Empty);
Assert.That(checkedProperties, Is.GreaterThan(300));
Assert.That(checkedProperties, Is.GreaterThan(200));
}
[Test]

View file

@ -0,0 +1,10 @@
# thing-contract-parity
The names the C# package `GameCult.Thing.Contract` gives its types. The C# package owns them because CultLib's C# reference compares catalog type names when a store reopens, and every runtime package is held to the C# names.
| File | Rows |
|---|---|
| `catalog-members.tsv` | One row per document slot, `schemaId`, `slot`, `memberName`, `typeName`, as `Wire.Registry`'s catalog entries print them. A list slot reads `System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.X>`. |
| `type-names.txt` | The full name of every public type in `GameCult.Thing.Contract`, sorted ordinally. |
`tests/GameCult.Thing.Contract.Tests/NameSetTests.cs` fails when either file disagrees with the C# types. These files change only in the commit that changes the C# types, and the other runtime packages read them without writing them.

View file

@ -0,0 +1,27 @@
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 0 InvocationId System.String
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 1 SurfaceKey System.String
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 2 SurfaceVersion System.Int64
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 3 Command System.String
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 4 Payload GameCult.Thing.Contract.RecordPayload
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 5 Captured System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CapturedInput>
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 6 Client System.String
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 7 IssuedAt System.String
sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 8 Delegation System.String
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 0 ProviderId System.String
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 1 CatalogId System.String
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 2 Version System.Int64
sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 3 Assets System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.AssetEntry>
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 0 Publisher System.String
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 1 Name System.String
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 2 Version System.Int64
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 3 Params System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.ParamDecl>
sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 4 Body GameCult.Thing.Contract.Node
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 0 ProviderId System.String
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 1 Label System.String
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 2 Version System.Int64
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 3 Title System.String
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 4 Root GameCult.Thing.Contract.Node
sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 5 Commands System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CommandDecl>
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 0 InvocationId System.String
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 1 Seq System.Int32
sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 2 Outcome GameCult.Thing.Contract.Outcome
1 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 0 InvocationId System.String
2 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 1 SurfaceKey System.String
3 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 2 SurfaceVersion System.Int64
4 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 3 Command System.String
5 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 4 Payload GameCult.Thing.Contract.RecordPayload
6 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 5 Captured System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CapturedInput>
7 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 6 Client System.String
8 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 7 IssuedAt System.String
9 sha256:186674dff7658d23646b25b28f54d27b3f05cee34b51a631580be65002c41aa8 8 Delegation System.String
10 sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 0 ProviderId System.String
11 sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 1 CatalogId System.String
12 sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 2 Version System.Int64
13 sha256:3d751a6fb07487b0c84cf0ae1c4fe55e5ef0c1656eab41ca3236846a638bdd3f 3 Assets System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.AssetEntry>
14 sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 0 Publisher System.String
15 sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 1 Name System.String
16 sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 2 Version System.Int64
17 sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 3 Params System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.ParamDecl>
18 sha256:531f96346fbff5160daedc82ada2ad5cb3d0477631d2b580ccd1a2834f041b32 4 Body GameCult.Thing.Contract.Node
19 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 0 ProviderId System.String
20 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 1 Label System.String
21 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 2 Version System.Int64
22 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 3 Title System.String
23 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 4 Root GameCult.Thing.Contract.Node
24 sha256:da2ca90e225d803191fd9a31ad1894e7dfab186f82e892717be8130d2e7ebf33 5 Commands System.Collections.Generic.IReadOnlyList<GameCult.Thing.Contract.CommandDecl>
25 sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 0 InvocationId System.String
26 sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 1 Seq System.Int32
27 sha256:e0273199abadb60da0bd9a7bcb863e131ef08bb2284f7eb729f76159caa369fd 2 Outcome GameCult.Thing.Contract.Outcome

View file

@ -0,0 +1,168 @@
GameCult.Thing.Contract.AffixFormat
GameCult.Thing.Contract.Anchor
GameCult.Thing.Contract.AnchorNode
GameCult.Thing.Contract.AnchorViewport
GameCult.Thing.Contract.Arg
GameCult.Thing.Contract.ArgValue
GameCult.Thing.Contract.ArgValueBind
GameCult.Thing.Contract.AssetCatalog
GameCult.Thing.Contract.AssetEntry
GameCult.Thing.Contract.Bind
GameCult.Thing.Contract.BoolArg
GameCult.Thing.Contract.BoolLiteral
GameCult.Thing.Contract.BoolSource
GameCult.Thing.Contract.BoolSourceBound
GameCult.Thing.Contract.BoolSourceParam
GameCult.Thing.Contract.BoolValue
GameCult.Thing.Contract.Border
GameCult.Thing.Contract.BoundOptions
GameCult.Thing.Contract.BoxNode
GameCult.Thing.Contract.BoxStyle
GameCult.Thing.Contract.ButtonNode
GameCult.Thing.Contract.CapturedInput
GameCult.Thing.Contract.ChoiceValue
GameCult.Thing.Contract.CollectionArg
GameCult.Thing.Contract.CollectionSource
GameCult.Thing.Contract.CollectionSourceMember
GameCult.Thing.Contract.CollectionSourceParam
GameCult.Thing.Contract.ColorRef
GameCult.Thing.Contract.CommandArg
GameCult.Thing.Contract.CommandDecl
GameCult.Thing.Contract.CommandIdRef
GameCult.Thing.Contract.CommandInvocation
GameCult.Thing.Contract.CommandRef
GameCult.Thing.Contract.CommandRefParam
GameCult.Thing.Contract.CommandResult
GameCult.Thing.Contract.CompletedOutcome
GameCult.Thing.Contract.CompositionDocument
GameCult.Thing.Contract.CompositionRef
GameCult.Thing.Contract.ContentAlign
GameCult.Thing.Contract.ContentAlignAuto
GameCult.Thing.Contract.ContentAlignCenter
GameCult.Thing.Contract.ContentAlignEnd
GameCult.Thing.Contract.ContentAlignSpaceAround
GameCult.Thing.Contract.ContentAlignSpaceBetween
GameCult.Thing.Contract.ContentAlignSpaceEvenly
GameCult.Thing.Contract.ContentAlignStart
GameCult.Thing.Contract.ContentAlignStretch
GameCult.Thing.Contract.DeniedOutcome
GameCult.Thing.Contract.Dimension
GameCult.Thing.Contract.DimensionAuto
GameCult.Thing.Contract.DimensionContent
GameCult.Thing.Contract.DimensionFr
GameCult.Thing.Contract.DimensionPercent
GameCult.Thing.Contract.DimensionPx
GameCult.Thing.Contract.Direction
GameCult.Thing.Contract.DirectionColumn
GameCult.Thing.Contract.DirectionColumnReverse
GameCult.Thing.Contract.DirectionRow
GameCult.Thing.Contract.DirectionRowReverse
GameCult.Thing.Contract.Display
GameCult.Thing.Contract.DisplayBlock
GameCult.Thing.Contract.DisplayFlex
GameCult.Thing.Contract.DisplayGrid
GameCult.Thing.Contract.Edges
GameCult.Thing.Contract.Effect
GameCult.Thing.Contract.EmbedNode
GameCult.Thing.Contract.FailedOutcome
GameCult.Thing.Contract.Format
GameCult.Thing.Contract.FormatDuration
GameCult.Thing.Contract.FormatTimestamp
GameCult.Thing.Contract.ImageFit
GameCult.Thing.Contract.ImageFitContain
GameCult.Thing.Contract.ImageFitCover
GameCult.Thing.Contract.ImageFitFill
GameCult.Thing.Contract.ImageFitUnscaled
GameCult.Thing.Contract.ImageNode
GameCult.Thing.Contract.InputKind
GameCult.Thing.Contract.InputKindNumber
GameCult.Thing.Contract.InputKindText
GameCult.Thing.Contract.InputValue
GameCult.Thing.Contract.ItemAlign
GameCult.Thing.Contract.ItemAlignAuto
GameCult.Thing.Contract.ItemAlignBaseline
GameCult.Thing.Contract.ItemAlignCenter
GameCult.Thing.Contract.ItemAlignEnd
GameCult.Thing.Contract.ItemAlignStart
GameCult.Thing.Contract.ItemAlignStretch
GameCult.Thing.Contract.LiteralOptions
GameCult.Thing.Contract.MemberIndex
GameCult.Thing.Contract.MemberName
GameCult.Thing.Contract.MemberPath
GameCult.Thing.Contract.MemberStep
GameCult.Thing.Contract.Node
GameCult.Thing.Contract.NodeCommon
GameCult.Thing.Contract.NumberArg
GameCult.Thing.Contract.NumberFormat
GameCult.Thing.Contract.NumberLiteral
GameCult.Thing.Contract.NumberSource
GameCult.Thing.Contract.NumberSourceBound
GameCult.Thing.Contract.NumberSourceParam
GameCult.Thing.Contract.NumberValue
GameCult.Thing.Contract.OpenExternalEffect
GameCult.Thing.Contract.OpenSurfaceEffect
GameCult.Thing.Contract.OptionList
GameCult.Thing.Contract.Outcome
GameCult.Thing.Contract.Overflow
GameCult.Thing.Contract.OverflowClip
GameCult.Thing.Contract.OverflowScroll
GameCult.Thing.Contract.OverflowVisible
GameCult.Thing.Contract.OverlayNode
GameCult.Thing.Contract.Paint
GameCult.Thing.Contract.ParamDecl
GameCult.Thing.Contract.ParamKind
GameCult.Thing.Contract.ParamKindBind
GameCult.Thing.Contract.ParamKindBool
GameCult.Thing.Contract.ParamKindCollection
GameCult.Thing.Contract.ParamKindCommand
GameCult.Thing.Contract.ParamKindNumber
GameCult.Thing.Contract.ParamKindSlot
GameCult.Thing.Contract.ParamKindText
GameCult.Thing.Contract.PendingOutcome
GameCult.Thing.Contract.PlotMark
GameCult.Thing.Contract.PlotMarkArea
GameCult.Thing.Contract.PlotMarkBar
GameCult.Thing.Contract.PlotMarkLine
GameCult.Thing.Contract.PlotNode
GameCult.Thing.Contract.Position
GameCult.Thing.Contract.PositionAbsolute
GameCult.Thing.Contract.PositionRelative
GameCult.Thing.Contract.RecordPayload
GameCult.Thing.Contract.RecordsSource
GameCult.Thing.Contract.RepeatNode
GameCult.Thing.Contract.Rgba
GameCult.Thing.Contract.Role
GameCult.Thing.Contract.RoleDialog
GameCult.Thing.Contract.RoleGroup
GameCult.Thing.Contract.RoleHeading
GameCult.Thing.Contract.RoleList
GameCult.Thing.Contract.RoleListItem
GameCult.Thing.Contract.RoleRegion
GameCult.Thing.Contract.RoleToolbar
GameCult.Thing.Contract.SelectNode
GameCult.Thing.Contract.SelectOption
GameCult.Thing.Contract.Size2
GameCult.Thing.Contract.SliderNode
GameCult.Thing.Contract.SlotArg
GameCult.Thing.Contract.StateRef
GameCult.Thing.Contract.StateSource
GameCult.Thing.Contract.StateSourceItem
GameCult.Thing.Contract.SurfaceDocument
GameCult.Thing.Contract.TextArg
GameCult.Thing.Contract.TextInputNode
GameCult.Thing.Contract.TextLiteral
GameCult.Thing.Contract.TextNode
GameCult.Thing.Contract.TextSource
GameCult.Thing.Contract.TextSourceBound
GameCult.Thing.Contract.TextSourceParam
GameCult.Thing.Contract.TextValue
GameCult.Thing.Contract.ThemeToken
GameCult.Thing.Contract.ThingDomain
GameCult.Thing.Contract.ThingDomainException
GameCult.Thing.Contract.ThingKeys
GameCult.Thing.Contract.ToggleNode
GameCult.Thing.Contract.UseNode
GameCult.Thing.Contract.Wrap
GameCult.Thing.Contract.WrapNoWrap
GameCult.Thing.Contract.WrapWrap
GameCult.Thing.Contract.WrapWrapReverse