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

269 lines
9.9 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(),
PlotInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "plots", ActivityAssetCatalog.EnumerateLivePlotIds(), plots, PlotInterestCatalog.GetOrFallback);
CatalogCoverageDiff eras = CatalogCoverageAudit.Diff(
"eras",
ActivityAssetCatalog.EnumerateLiveEraIds(),
EraInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "eras", ActivityAssetCatalog.EnumerateLiveEraIds(), eras, EraInterestCatalog.GetOrFallback);
CatalogCoverageDiff books = CatalogCoverageAudit.Diff(
"books",
ActivityAssetCatalog.EnumerateLiveBookTypeIds(),
BookInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "books", ActivityAssetCatalog.EnumerateLiveBookTypeIds(), books, BookInterestCatalog.GetOrFallback);
CatalogCoverageDiff traits = CatalogCoverageAudit.Diff(
"traits",
ActivityAssetCatalog.EnumerateLiveTraitIds(),
TraitInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "traits", ActivityAssetCatalog.EnumerateLiveTraitIds(), traits, TraitInterestCatalog.GetOrFallback);
CatalogCoverageDiff spells = CatalogCoverageAudit.Diff(
"spells",
ActivityAssetCatalog.EnumerateLiveSpellIds(),
SpellInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "spells", ActivityAssetCatalog.EnumerateLiveSpellIds(), spells, SpellInterestCatalog.GetOrFallback);
CatalogCoverageDiff powers = CatalogCoverageAudit.Diff(
"powers",
ActivityAssetCatalog.EnumerateLivePowerIds(),
PowerInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "powers", ActivityAssetCatalog.EnumerateLivePowerIds(), powers, PowerInterestCatalog.GetOrFallback);
CatalogCoverageDiff decisions = CatalogCoverageAudit.Diff(
"decisions",
ActivityAssetCatalog.EnumerateLiveDecisionIds(),
DecisionInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "decisions", ActivityAssetCatalog.EnumerateLiveDecisionIds(), decisions, DecisionInterestCatalog.GetOrFallback);
CatalogCoverageDiff items = CatalogCoverageAudit.Diff(
"items",
ActivityAssetCatalog.EnumerateLiveItemIds(),
ItemInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "items", ActivityAssetCatalog.EnumerateLiveItemIds(), items, ItemInterestCatalog.GetOrFallback);
CatalogCoverageDiff subspTraits = CatalogCoverageAudit.Diff(
"subspecies_traits",
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds(),
SubspeciesTraitInterestCatalog.AuthoredIds);
AppendLibraryDomain(
tsv,
"subspecies_traits",
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds(),
subspTraits,
SubspeciesTraitInterestCatalog.GetOrFallback);
CatalogCoverageDiff genes = CatalogCoverageAudit.Diff(
"genes",
ActivityAssetCatalog.EnumerateLiveGeneIds(),
GeneInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "genes", ActivityAssetCatalog.EnumerateLiveGeneIds(), genes, GeneInterestCatalog.GetOrFallback);
CatalogCoverageDiff phenotypes = CatalogCoverageAudit.Diff(
"phenotypes",
ActivityAssetCatalog.EnumerateLivePhenotypeIds(),
PhenotypeInterestCatalog.AuthoredIds);
AppendLibraryDomain(
tsv,
"phenotypes",
ActivityAssetCatalog.EnumerateLivePhenotypeIds(),
phenotypes,
PhenotypeInterestCatalog.GetOrFallback);
CatalogCoverageDiff worldLaws = CatalogCoverageAudit.Diff(
"world_laws",
ActivityAssetCatalog.EnumerateLiveWorldLawIds(),
WorldLawInterestCatalog.AuthoredIds);
AppendLibraryDomain(
tsv,
"world_laws",
ActivityAssetCatalog.EnumerateLiveWorldLawIds(),
worldLaws,
WorldLawInterestCatalog.GetOrFallback);
CatalogCoverageDiff biomes = CatalogCoverageAudit.Diff(
"biomes",
ActivityAssetCatalog.EnumerateLiveBiomeIds(),
BiomeInterestCatalog.AuthoredIds);
AppendLibraryDomain(tsv, "biomes", ActivityAssetCatalog.EnumerateLiveBiomeIds(), biomes, BiomeInterestCatalog.GetOrFallback);
int relMissing = 0;
int relTotal = 0;
foreach (string id in RelationshipEventCatalog.AuthoredIds)
{
relTotal++;
DiscreteEventEntry entry = RelationshipEventCatalog.GetOrFallback(id);
bool ok = RelationshipEventCatalog.HasAuthored(id) && !entry.IsFallback;
if (!ok)
{
relMissing++;
}
tsv.Append("relationship\t").Append(id).Append('\t')
.Append(ok ? "1" : "0").Append('\t')
.Append(entry.OwnsCamera).Append('\t')
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
.Append(ok ? "ok" : "missing_authored").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;
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}";
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.OwnsCamera).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();
}
}
}