Caching: README section on element ids; the hex refusal names the derived form for readable ids

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 20:31:44 +02:00
commit e3f497197d
3 changed files with 33 additions and 1 deletions

View file

@ -290,7 +290,7 @@ namespace GameCult.Caching
continue;
if (shape.Derive == null && !IsRandomId(id!))
throw new CultElementIdException(
$"Record {rootPath}: element id '{id}' in the list at {path} is not 12 lowercase hex characters.",
$"Record {rootPath}: element id '{id}' in the list at {path} is not 12 lowercase hex characters. An id you write by hand must be 12 lowercase hex; for readable ids, derive them from a member with [CultElementId(nameof(Member))].",
rootPath, path, id);
if (!taken.Add(id!))
throw Duplicate(id!, path);

View file

@ -188,6 +188,31 @@ for (var row = 0; row < table.Count; row++)
cache-owned physical table for hot loops and diagnostics; it is not a second
domain model callers have to maintain.
## Element ids
A document type that holds a list of objects gives every object type in that list
(union arms included) a string member marked `[CultElementId]`, next to its
`[Key]`. The id is what lets a variant override one list element instead of the
whole list. Lists of strings, numbers and other non-object values need none.
```csharp
[MessagePackObject]
public sealed class Line
{
[Key(0)] [CultElementId] public string Id = string.Empty;
[Key(1)] public string Text = string.Empty;
}
```
Leave `Id` empty and the cache mints a random id when the record is written. An
id you set yourself must be exactly 12 lowercase hex characters
(`0123456789ab`); anything else is refused with a `CultElementIdException`.
For readable ids, derive them: `[CultElementId(nameof(Name))]` makes an unset id
the text of that member (a string or number on the same type, unique within its
list), and a derived id may be any text. A readable name set by hand on a
non-derived list id is refused; the same member outside a list is unconstrained.
## Persistence discipline
Persistence stays manual by default. Mutations mark the cache and attached

View file

@ -135,6 +135,13 @@ namespace GameCult.Caching.Tests
Assert.Throws<CultElementIdException>(() => Assign(grid, "r", deterministic: false));
}
[Test]
public void TheHexRefusalNamesTheDerivedFormForReadableIds()
{
var refusal = Assert.Throws<CultElementIdException>(() => Assign(new HHasIds { Items = { new EIded { Id = "root" } } }, "r", deterministic: false))!;
Assert.That(refusal.Message, Does.Contain("12 lowercase hex").And.Contain("[CultElementId(nameof(Member))]"));
}
[Test]
public void ARandomIdMustBeTwelveLowercaseHexCharactersAndADerivedIdNeedOnlyBeText()
{