VerseGrammar: Open refuses a role named actor and two verbs with one name; edge and command-update pins

ValidateAll (one owner, called by AetheriaStores.Open) refuses duplicate verb names; Validate refuses the reserved role
name actor. Tests pin the name-class edges (a z 0 9 _), a null role element, the starter verb replaced when only its
description or render path differs, the grammar global updated in place, and the fixture's second allegiance and zero-role
speak. The test's scoped registry is TestCatalog.Registry(). Builds; unverified until the full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 14:02:10 +02:00
commit 972848cb5f
3 changed files with 96 additions and 7 deletions

View file

@ -40,7 +40,7 @@ public static class AetheriaStores
// cut.md), stat modifiers, role usage (Cut 7). Fails loudly, naming the item.
foreach (var data in cache.GetAll<EquippableItemData>()) CultRecordRefs.Validate(data);
foreach (var data in cache.GetAll<ConsumableItemData>()) CultRecordRefs.Validate(data);
foreach (var verb in cache.GetAll<VerseVerb>()) VerseGrammarValidation.Validate(verb);
VerseGrammarValidation.ValidateAll(cache.GetAll<VerseVerb>());
return cache;
}
catch

View file

@ -57,6 +57,9 @@ public static class VerseGrammarValidation
public const int VerbNameMaxLength = 48;
public const int RoleNameMaxLength = 32;
// The invoking subject, never a role (Ghostlight refuses a world affordance that names it: ReservedRole).
public const string ActorRole = "actor";
// Canonical: a lower-case letter, then lower-case letters, digits and underscores, at most maxLength characters.
private static bool Canonical(string name, int maxLength)
{
@ -67,6 +70,18 @@ public static class VerseGrammarValidation
return true;
}
// Every verb valid and no two sharing a name: a verb is found by name (Ghostlight refuses DuplicateAffordanceKind).
public static void ValidateAll(IEnumerable<VerseVerb> verbs)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var verb in verbs)
{
Validate(verb);
if (!seen.Add(verb.Name))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\": duplicate verb name.");
}
}
// Canonical names, unique role names, defined enum values. A failure names the field and the rule, and the verb once
// its name is known canonical; it never prints a value the author wrote that is itself the fault.
public static void Validate(VerseVerb verb)
@ -82,6 +97,8 @@ public static class VerseGrammarValidation
{
if (role == null || !Canonical(role.Name, RoleNameMaxLength))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Name: not a canonical role name ([a-z][a-z0-9_], 1 to {RoleNameMaxLength} characters).");
if (role.Name == ActorRole)
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Name: reserved for the invoking subject, never a role.");
if (!seen.Add(role.Name))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Name: duplicate role name.");
if (!Enum.IsDefined(typeof(VerseReferentKind), role.Binds))