feat(bootstrap): validate seed event_type against MissionEventKeys registry

Achievement, mission, and BP monthly mission importers now throw at bootstrap
if any seed row references an event_type that doesn't start with a registered
top-level prefix. Prevents drift: a typo in the seed JSON (or a prefix removed
from code without updating the seed) fails loudly at startup instead of
silently producing counters no emitter writes to.

Verified all 12 top-level prefixes currently in the seed JSON pass validation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 12:59:48 -04:00
parent 652011ea74
commit 0f6c8d5e41
3 changed files with 49 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ using SVSim.Bootstrap.Models.Seed;
using SVSim.Database; using SVSim.Database;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Models; using SVSim.Database.Models;
using SVSim.Database.Services;
namespace SVSim.Bootstrap.Importers; namespace SVSim.Bootstrap.Importers;
@@ -24,6 +25,22 @@ public class AchievementCatalogImporter
var existing = await context.AchievementCatalog var existing = await context.AchievementCatalog
.ToDictionaryAsync(e => (e.AchievementType, e.Level)); .ToDictionaryAsync(e => (e.AchievementType, e.Level));
// Fail fast on drift between seed event_type values and the code-side registry.
// Missing prefixes silently produce counters no emitter ever writes to; typos likewise.
var unregistered = seed
.Where(s => s.EventType is not null && !MissionEventKeys.IsRegistered(s.EventType))
.Select(s => s.EventType!)
.Distinct()
.OrderBy(x => x)
.ToList();
if (unregistered.Count > 0)
{
throw new InvalidOperationException(
"[AchievementCatalogImporter] seed rows reference unregistered event_type(s): "
+ string.Join(", ", unregistered)
+ ". Add the top-level prefix to MissionEventKeys.RegisteredPrefixes, or fix the seed.");
}
int created = 0, updated = 0; int created = 0, updated = 0;
var unmappedTypes = new HashSet<int>(); var unmappedTypes = new HashSet<int>();
foreach (var s in seed) foreach (var s in seed)

View File

@@ -3,6 +3,7 @@ using SVSim.Bootstrap.Models.Seed;
using SVSim.Database; using SVSim.Database;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Models; using SVSim.Database.Models;
using SVSim.Database.Services;
namespace SVSim.Bootstrap.Importers; namespace SVSim.Bootstrap.Importers;
@@ -22,6 +23,21 @@ public class BattlePassMonthlyMissionImporter
return 0; return 0;
} }
// Fail fast on drift between seed event_type values and the code-side registry.
var unregistered = seed
.Where(s => s.EventType is not null && !MissionEventKeys.IsRegistered(s.EventType))
.Select(s => s.EventType!)
.Distinct()
.OrderBy(x => x)
.ToList();
if (unregistered.Count > 0)
{
throw new InvalidOperationException(
"[BattlePassMonthlyMissionImporter] seed rows reference unregistered event_type(s): "
+ string.Join(", ", unregistered)
+ ". Add the top-level prefix to MissionEventKeys.RegisteredPrefixes, or fix the seed.");
}
var existing = await context.BattlePassMonthlyMissions var existing = await context.BattlePassMonthlyMissions
.ToDictionaryAsync(e => (e.Year, e.Month, e.OrderNum)); .ToDictionaryAsync(e => (e.Year, e.Month, e.OrderNum));
int created = 0, updated = 0; int created = 0, updated = 0;

View File

@@ -3,6 +3,7 @@ using SVSim.Bootstrap.Models.Seed;
using SVSim.Database; using SVSim.Database;
using SVSim.Database.Enums; using SVSim.Database.Enums;
using SVSim.Database.Models; using SVSim.Database.Models;
using SVSim.Database.Services;
namespace SVSim.Bootstrap.Importers; namespace SVSim.Bootstrap.Importers;
@@ -21,6 +22,21 @@ public class MissionCatalogImporter
return 0; return 0;
} }
// Fail fast on drift between seed event_type values and the code-side registry.
var unregistered = seed
.Where(s => s.EventType is not null && !MissionEventKeys.IsRegistered(s.EventType))
.Select(s => s.EventType!)
.Distinct()
.OrderBy(x => x)
.ToList();
if (unregistered.Count > 0)
{
throw new InvalidOperationException(
"[MissionCatalogImporter] seed rows reference unregistered event_type(s): "
+ string.Join(", ", unregistered)
+ ". Add the top-level prefix to MissionEventKeys.RegisteredPrefixes, or fix the seed.");
}
var existing = await context.MissionCatalog.ToDictionaryAsync(e => e.Id); var existing = await context.MissionCatalog.ToDictionaryAsync(e => e.Id);
int created = 0, updated = 0; int created = 0, updated = 0;
var unmapped = new List<int>(); var unmapped = new List<int>();