worldbox-observer-mod/IdleSpectator/CatalogCoverageAudit.cs
2026-07-16 19:35:24 -05:00

355 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace IdleSpectator;
public sealed class CatalogCoverageDiff
{
public string Domain = "";
public int LiveTotal;
public int AuthoredTotal;
public int Missing;
public int Orphan;
public readonly List<string> MissingIds = new List<string>();
public readonly List<string> OrphanIds = new List<string>();
public bool Passed => Missing == 0 && Orphan == 0;
public string Detail =>
$"{Domain}: live={LiveTotal} authored={AuthoredTotal} missing={Missing} orphan={Orphan}";
}
/// <summary>Shared live-vs-authored coverage audit used by domain event catalogs.</summary>
public static class CatalogCoverageAudit
{
public static CatalogCoverageDiff Diff(
string domain,
IEnumerable<string> liveIds,
IEnumerable<string> authoredIds)
{
var live = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var authored = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var diff = new CatalogCoverageDiff { Domain = domain ?? "domain" };
if (liveIds != null)
{
foreach (string id in liveIds)
{
if (string.IsNullOrEmpty(id))
{
continue;
}
live.Add(id.Trim());
}
}
if (authoredIds != null)
{
foreach (string id in authoredIds)
{
if (string.IsNullOrEmpty(id))
{
continue;
}
authored.Add(id.Trim());
}
}
diff.LiveTotal = live.Count;
diff.AuthoredTotal = authored.Count;
foreach (string id in live)
{
if (!authored.Contains(id))
{
diff.Missing++;
diff.MissingIds.Add(id);
}
}
foreach (string id in authored)
{
if (!live.Contains(id))
{
diff.Orphan++;
diff.OrphanIds.Add(id);
}
}
return diff;
}
public static void WriteReport(string outputDirectory, string fileName, string body)
{
if (string.IsNullOrEmpty(outputDirectory) || string.IsNullOrEmpty(fileName))
{
return;
}
try
{
Directory.CreateDirectory(outputDirectory);
File.WriteAllText(Path.Combine(outputDirectory, fileName), body ?? "");
}
catch
{
// ignore IO
}
}
}
public sealed class DomainEventAuditResult
{
public bool Passed;
public string Detail = "";
}
/// <summary>Coverage audits for relationship discrete events + live plot/era/book libraries.</summary>
public static class DomainEventHarness
{
public static DomainEventAuditResult RunAudit(string outputDirectory)
{
var tsv = new StringBuilder();
tsv.AppendLine("domain\tid\tauthored\towns_camera\tevent_strength\tnotes");
CatalogCoverageDiff plots = CatalogCoverageAudit.Diff(
"plots",
ActivityAssetCatalog.EnumerateLivePlotIds(),
EventCatalog.Plot.AuthoredIds);
AppendLibraryDomain(tsv, "plots", ActivityAssetCatalog.EnumerateLivePlotIds(), plots, EventCatalog.Plot.GetOrFallback);
CatalogCoverageDiff eras = CatalogCoverageAudit.Diff(
"eras",
ActivityAssetCatalog.EnumerateLiveEraIds(),
EventCatalog.Era.AuthoredIds);
AppendLibraryDomain(tsv, "eras", ActivityAssetCatalog.EnumerateLiveEraIds(), eras, EventCatalog.Era.GetOrFallback);
CatalogCoverageDiff books = CatalogCoverageAudit.Diff(
"books",
ActivityAssetCatalog.EnumerateLiveBookTypeIds(),
EventCatalog.Book.AuthoredIds);
AppendLibraryDomain(tsv, "books", ActivityAssetCatalog.EnumerateLiveBookTypeIds(), books, EventCatalog.Book.GetOrFallback);
CatalogCoverageDiff traits = CatalogCoverageAudit.Diff(
"traits",
ActivityAssetCatalog.EnumerateLiveTraitIds(),
EventCatalog.Trait.AuthoredIds);
AppendLibraryDomain(tsv, "traits", ActivityAssetCatalog.EnumerateLiveTraitIds(), traits, EventCatalog.Trait.GetOrFallback);
CatalogCoverageDiff spells = CatalogCoverageAudit.Diff(
"spells",
ActivityAssetCatalog.EnumerateLiveSpellIds(),
EventCatalog.Spell.AuthoredIds);
AppendLibraryDomain(tsv, "spells", ActivityAssetCatalog.EnumerateLiveSpellIds(), spells, EventCatalog.Spell.GetOrFallback);
CatalogCoverageDiff powers = CatalogCoverageAudit.Diff(
"powers",
ActivityAssetCatalog.EnumerateLivePowerIds(),
EventCatalog.Power.AuthoredIds);
AppendLibraryDomain(tsv, "powers", ActivityAssetCatalog.EnumerateLivePowerIds(), powers, EventCatalog.Power.GetOrFallback);
CatalogCoverageDiff decisions = CatalogCoverageAudit.Diff(
"decisions",
ActivityAssetCatalog.EnumerateLiveDecisionIds(),
EventCatalog.Decision.AuthoredIds);
AppendLibraryDomain(tsv, "decisions", ActivityAssetCatalog.EnumerateLiveDecisionIds(), decisions, EventCatalog.Decision.GetOrFallback);
CatalogCoverageDiff items = CatalogCoverageAudit.Diff(
"items",
ActivityAssetCatalog.EnumerateLiveItemIds(),
EventCatalog.Item.AuthoredIds);
AppendLibraryDomain(tsv, "items", ActivityAssetCatalog.EnumerateLiveItemIds(), items, EventCatalog.Item.GetOrFallback);
CatalogCoverageDiff subspTraits = CatalogCoverageAudit.Diff(
"subspecies_traits",
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds(),
EventCatalog.SubspeciesTrait.AuthoredIds);
AppendLibraryDomain(
tsv,
"subspecies_traits",
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds(),
subspTraits,
EventCatalog.SubspeciesTrait.GetOrFallback);
CatalogCoverageDiff genes = CatalogCoverageAudit.Diff(
"genes",
ActivityAssetCatalog.EnumerateLiveGeneIds(),
EventCatalog.Gene.AuthoredIds);
AppendLibraryDomain(tsv, "genes", ActivityAssetCatalog.EnumerateLiveGeneIds(), genes, EventCatalog.Gene.GetOrFallback);
CatalogCoverageDiff phenotypes = CatalogCoverageAudit.Diff(
"phenotypes",
ActivityAssetCatalog.EnumerateLivePhenotypeIds(),
EventCatalog.Phenotype.AuthoredIds);
AppendLibraryDomain(
tsv,
"phenotypes",
ActivityAssetCatalog.EnumerateLivePhenotypeIds(),
phenotypes,
EventCatalog.Phenotype.GetOrFallback);
CatalogCoverageDiff worldLaws = CatalogCoverageAudit.Diff(
"world_laws",
ActivityAssetCatalog.EnumerateLiveWorldLawIds(),
EventCatalog.WorldLaw.AuthoredIds);
AppendLibraryDomain(
tsv,
"world_laws",
ActivityAssetCatalog.EnumerateLiveWorldLawIds(),
worldLaws,
EventCatalog.WorldLaw.GetOrFallback);
CatalogCoverageDiff biomes = CatalogCoverageAudit.Diff(
"biomes",
ActivityAssetCatalog.EnumerateLiveBiomeIds(),
EventCatalog.Biome.AuthoredIds);
AppendLibraryDomain(tsv, "biomes", ActivityAssetCatalog.EnumerateLiveBiomeIds(), biomes, EventCatalog.Biome.GetOrFallback);
CatalogCoverageDiff cultureTraits = CatalogCoverageAudit.Diff(
"culture_traits",
ActivityAssetCatalog.EnumerateLiveCultureTraitIds(),
EventCatalog.CultureTrait.AuthoredIds);
AppendLibraryDomain(
tsv,
"culture_traits",
ActivityAssetCatalog.EnumerateLiveCultureTraitIds(),
cultureTraits,
EventCatalog.CultureTrait.GetOrFallback);
CatalogCoverageDiff religionTraits = CatalogCoverageAudit.Diff(
"religion_traits",
ActivityAssetCatalog.EnumerateLiveReligionTraitIds(),
EventCatalog.ReligionTrait.AuthoredIds);
AppendLibraryDomain(
tsv,
"religion_traits",
ActivityAssetCatalog.EnumerateLiveReligionTraitIds(),
religionTraits,
EventCatalog.ReligionTrait.GetOrFallback);
CatalogCoverageDiff clanTraits = CatalogCoverageAudit.Diff(
"clan_traits",
ActivityAssetCatalog.EnumerateLiveClanTraitIds(),
EventCatalog.ClanTrait.AuthoredIds);
AppendLibraryDomain(
tsv,
"clan_traits",
ActivityAssetCatalog.EnumerateLiveClanTraitIds(),
clanTraits,
EventCatalog.ClanTrait.GetOrFallback);
CatalogCoverageDiff languageTraits = CatalogCoverageAudit.Diff(
"language_traits",
ActivityAssetCatalog.EnumerateLiveLanguageTraitIds(),
EventCatalog.LanguageTrait.AuthoredIds);
AppendLibraryDomain(
tsv,
"language_traits",
ActivityAssetCatalog.EnumerateLiveLanguageTraitIds(),
languageTraits,
EventCatalog.LanguageTrait.GetOrFallback);
int relMissing = 0;
int relTotal = 0;
foreach (string id in EventCatalog.Relationship.AuthoredIds)
{
relTotal++;
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback(id);
bool ok = EventCatalog.Relationship.HasAuthored(id) && !entry.IsFallback;
if (!ok)
{
relMissing++;
}
tsv.Append("relationship\t").Append(id).Append('\t')
.Append(ok ? "1" : "0").Append('\t')
.Append(entry.CreatesInterest).Append('\t')
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
.Append(ok ? "ok" : "missing_authored").AppendLine();
}
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
// Camera decisions must have authored ActionPhrase + EventStrength (event-catalog.json).
int decisionProseMissing = 0;
int decisionStrengthMissing = 0;
int decisionProseTotal = 0;
List<string> liveDecisions = ActivityAssetCatalog.EnumerateLiveDecisionIds();
for (int i = 0; i < liveDecisions.Count; i++)
{
string id = liveDecisions[i];
if (!EventCatalog.Decision.IsCameraWorthy(id))
{
continue;
}
decisionProseTotal++;
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
if (!EventCatalog.Decision.TryGetAuthoredActionPhrase(id, out _))
{
decisionProseMissing++;
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t\tmissing_action_phrase").AppendLine();
}
else if (entry.EventStrength <= 0f)
{
decisionStrengthMissing++;
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t")
.Append(entry.EventStrength.ToString("0.#"))
.Append("\tmissing_strength").AppendLine();
}
else
{
string phrase = EventCatalog.Decision.ActionPhraseFor(id);
tsv.Append("decision_prose\t").Append(id).Append("\t1\t1\t")
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
.Append(phrase).AppendLine();
}
}
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
bool passed = plots.Passed && eras.Passed && books.Passed && traits.Passed
&& spells.Passed && powers.Passed && decisions.Passed && items.Passed
&& subspTraits.Passed && genes.Passed && phenotypes.Passed
&& worldLaws.Passed && biomes.Passed
&& relMissing == 0 && relTotal > 0
&& decisionProseMissing == 0 && decisionStrengthMissing == 0
&& decisionProseTotal > 0;
string detail =
$"{plots.Detail}; {eras.Detail}; {books.Detail}; {traits.Detail}; "
+ $"{spells.Detail}; {powers.Detail}; {decisions.Detail}; {items.Detail}; "
+ $"{subspTraits.Detail}; {genes.Detail}; {phenotypes.Detail}; "
+ $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}; "
+ $"decision_prose: camera={decisionProseTotal} missing={decisionProseMissing} "
+ $"strengthMissing={decisionStrengthMissing} catalog={EventCatalogConfig.LoadedFromFile}";
return new DomainEventAuditResult { Passed = passed, Detail = detail };
}
private static void AppendLibraryDomain(
StringBuilder tsv,
string domain,
IEnumerable<string> liveIds,
CatalogCoverageDiff diff,
Func<string, DiscreteEventEntry> lookup)
{
foreach (string id in liveIds)
{
DiscreteEventEntry entry = lookup(id);
bool has = !entry.IsFallback;
tsv.Append(domain).Append('\t').Append(id).Append('\t')
.Append(has ? "1" : "0").Append('\t')
.Append(entry.CreatesInterest).Append('\t')
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
.Append(has ? "ok" : "missing_authored").AppendLine();
}
foreach (string id in diff.OrphanIds)
{
tsv.Append(domain).Append('\t').Append(id).Append("\t1\t\t0\torphan_authored").AppendLine();
}
}
}