diff --git a/SVSim.Bootstrap/Importers/GlobalsImporter.cs b/SVSim.Bootstrap/Importers/GlobalsImporter.cs deleted file mode 100644 index c87362f..0000000 --- a/SVSim.Bootstrap/Importers/GlobalsImporter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using SVSim.Database; - -namespace SVSim.Bootstrap.Importers; - -/// -/// Stub remaining after Stage 9C: the entire load-index → DB pipeline has been replaced by -/// per-domain importers in this folder (RotationConfigImporter, MyRotationImporter, -/// AvatarAbilityImporter, ArenaSeasonImporter, BattlePassImporter, DailyLoginBonusImporter, -/// PreReleaseInfoImporter, CardListsImporter, RotationFlagUpdater). Task 10 will delete this -/// class entirely; until then this stub keeps existing call sites compiling. -/// -public class GlobalsImporter -{ - public Task ImportAllAsync(SVSimDbContext context, string capturesDir) - { - // All work migrated to per-domain importers wired in Program.cs and - // SVSimTestFactory.SeedGlobalsAsync. Intentionally a no-op. - _ = context; - _ = capturesDir; - return Task.FromResult(0); - } -} diff --git a/SVSim.Bootstrap/Importers/ImporterBase.cs b/SVSim.Bootstrap/Importers/ImporterBase.cs index e0f7f09..46bbbdf 100644 --- a/SVSim.Bootstrap/Importers/ImporterBase.cs +++ b/SVSim.Bootstrap/Importers/ImporterBase.cs @@ -1,84 +1,13 @@ -using System.Text.Json; - namespace SVSim.Bootstrap.Importers; /// -/// Shared helpers for content importers. Loads a prod-capture JSON file by endpoint name from -/// a captures directory, returning the inner data element. Picks the latest matching dated -/// file (e.g. load-index-2026-05-23.json) if multiple exist for the same endpoint. +/// Tiny shared helper for content importers. Capture parsing has moved out of the bootstrap +/// project entirely (extractors under data_dumps/extract/ emit per-table seed JSON); +/// only the wire-date normaliser stays here because several seed-driven importers still need +/// to canonicalise prod-shaped timestamp strings. /// public static class ImporterBase { - /// - /// Returns the parsed .data JsonElement for the latest {endpoint}-*.json file in - /// , or null if no file matches. Logs a warning when missing — - /// caller decides whether absence is fatal. - /// - public static JsonElement? LoadCapture(string capturesDir, string endpoint) - { - if (!Directory.Exists(capturesDir)) - { - Console.Error.WriteLine($"[ImporterBase] Captures dir missing: {capturesDir}"); - return null; - } - - string? path = Directory.EnumerateFiles(capturesDir, $"{endpoint}-*.json") - .OrderByDescending(p => p) - .FirstOrDefault(); - - if (path is null) - { - Console.Error.WriteLine($"[ImporterBase] No capture found for endpoint '{endpoint}' in {capturesDir}"); - return null; - } - - using var fs = File.OpenRead(path); - using var doc = JsonDocument.Parse(fs); - if (!doc.RootElement.TryGetProperty("data", out var data)) - { - Console.Error.WriteLine($"[ImporterBase] Capture file {path} has no top-level 'data' property."); - return null; - } - // Clone so the JsonElement survives doc disposal. - return data.Clone(); - } - - /// - /// Generic upsert by primary key. Returns (created, updated, unchanged) counts. - /// is the desired state from the capture; rows are matched by - /// . mutates an existing row to - /// reflect incoming values and returns true if anything actually changed. - /// - public static (int created, int updated, int unchanged) Upsert( - IEnumerable incoming, - Dictionary existingByKey, - Func keySelector, - Action addToContext, - Func applyChanges) where TKey : notnull - { - int created = 0, updated = 0, unchanged = 0; - foreach (var item in incoming) - { - var key = keySelector(item); - if (existingByKey.TryGetValue(key, out var existing)) - { - if (applyChanges(existing, item)) updated++; - else unchanged++; - } - else - { - addToContext(item); - existingByKey[key] = item; - created++; - } - } - return (created, updated, unchanged); - } - - /// Serialize a JsonElement back to compact JSON text for jsonb storage. - public static string Serialize(JsonElement el) => - JsonSerializer.Serialize(el, new JsonSerializerOptions { WriteIndented = false }); - /// Parse a wire date that may be ISO ("2026-05-23T..."), space-separated ("2026-05-23 16:32:31"), or empty. public static DateTime ParseWireDateTime(string? s) { @@ -91,50 +20,4 @@ public static class ImporterBase } return DateTime.MinValue; } - - /// Read a JsonElement string/number property as long, defaulting on missing/null. - public static long GetLong(JsonElement el, string prop, long fallback = 0) - { - if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; - if (v.ValueKind == JsonValueKind.Number) return v.GetInt64(); - if (v.ValueKind == JsonValueKind.String && long.TryParse(v.GetString(), out var n)) return n; - return fallback; - } - - public static int GetInt(JsonElement el, string prop, int fallback = 0) - { - if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; - if (v.ValueKind == JsonValueKind.Number) return v.GetInt32(); - if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), out var n)) return n; - return fallback; - } - - public static string GetString(JsonElement el, string prop, string fallback = "") - { - if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; - return v.ValueKind == JsonValueKind.String ? v.GetString() ?? fallback : v.ToString(); - } - - public static bool GetBool(JsonElement el, string prop, bool fallback = false) - { - if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; - if (v.ValueKind == JsonValueKind.True) return true; - if (v.ValueKind == JsonValueKind.False) return false; - if (v.ValueKind == JsonValueKind.Number) return v.GetInt32() != 0; - if (v.ValueKind == JsonValueKind.String) - { - var s = v.GetString(); - if (bool.TryParse(s, out var b)) return b; - if (int.TryParse(s, out var i)) return i != 0; - } - return fallback; - } - - public static ulong GetULong(JsonElement el, string prop, ulong fallback = 0) - { - if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; - if (v.ValueKind == JsonValueKind.Number) return v.GetUInt64(); - if (v.ValueKind == JsonValueKind.String && ulong.TryParse(v.GetString(), out var n)) return n; - return fallback; - } } diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index d1dde7e..8b6ed5e 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -34,7 +34,7 @@ public static class Program Console.WriteLine($"[Bootstrap] Connection: {RedactPassword(opts.ConnectionString)}"); Console.WriteLine($"[Bootstrap] Reference CSVs: {opts.ReferenceDataDir}"); Console.WriteLine($"[Bootstrap] Cards file: {opts.CardsFile}"); - Console.WriteLine($"[Bootstrap] Captures: {opts.CapturesDir}"); + Console.WriteLine($"[Bootstrap] Seeds: {opts.SeedDir}"); var dbOptions = new DbContextOptionsBuilder() .UseNpgsql(opts.ConnectionString) @@ -75,9 +75,11 @@ public static class Program if (!opts.SkipGlobals) { - await new GlobalsImporter().ImportAllAsync(context, opts.CapturesDir); - - // Load-index seed pipeline (Stage 9C replaced the old in-GlobalsImporter capture-parsing). + // Per-domain seed pipeline. The legacy GlobalsImporter that parsed prod-captured + // /load/index, /mypage/index, /deck/info wire payloads directly is gone — capture + // → seed transformation lives in data_dumps/extract/*; importers below just + // deserialise the per-table JSON files in SVSim.Bootstrap/Data/seeds/. + // // RotationConfigImporter writes the Rotation GameConfig section that RotationFlagUpdater // reads; CardImporter ran earlier in the !SkipCards block so CardSets are populated. await new RotationConfigImporter().ImportAsync(context, opts.SeedDir); @@ -136,7 +138,6 @@ public static class Program private static BootstrapOptions? ParseArgs(string[] args) { string? cards = null; - string? captures = null; string? referenceDataDir = null; string? connection = null; bool skipReference = false; @@ -152,7 +153,6 @@ public static class Program switch (a) { case "--cards": cards = NextArg(args, ref i); break; - case "--captures": captures = NextArg(args, ref i); break; case "--reference-data-dir": referenceDataDir = NextArg(args, ref i); break; case "--connection-string": connection = NextArg(args, ref i); break; case "--skip-reference": skipReference = true; break; @@ -170,15 +170,13 @@ public static class Program } // All bootstrap inputs ship in-project under SVSim.Bootstrap/Data/, copied next to the - // binary on build. The --cards/--captures/--reference-data-dir flags are ad-hoc overrides + // binary on build. The --cards/--reference-data-dir flags are ad-hoc overrides // (e.g. point at a fresh loader dump before promoting it into the project). string baseDir = AppContext.BaseDirectory; string shippedDataDir = Path.Combine(baseDir, "Data"); - string shippedCaptures = Path.Combine(shippedDataDir, "prod-captures"); string shippedCardsFile = Path.Combine(shippedDataDir, "cards.json"); string cardsFile = cards ?? positionalCards ?? shippedCardsFile; - string capturesDir = captures ?? shippedCaptures; string refDir = referenceDataDir ?? shippedDataDir; string shippedStoryDir = Path.Combine(shippedDataDir, "story"); string storyDir = storyDataDir ?? shippedStoryDir; @@ -189,7 +187,7 @@ public static class Program ?? DefaultConnectionString; return new BootstrapOptions( - cardsFile, capturesDir, refDir, connStr, skipReference, skipCards, skipGlobals, + cardsFile, refDir, connStr, skipReference, skipCards, skipGlobals, skipStory, storyDir, shippedSeedDir); } @@ -212,23 +210,24 @@ public static class Program " loader dump) — promote into Data/ when you're ready to make it permanent.\n" + "\n" + " --cards Override path to cards.json (default: shipped Data/cards.json)\n" + - " --captures Override path to prod-captures directory\n" + - " (default: shipped Data/prod-captures)\n" + " --reference-data-dir Override reference CSV directory (default: shipped Data/)\n" + " --connection-string Postgres connection (or NPGSQL_CONNECTION env var,\n" + $" then \"{DefaultConnectionString}\")\n" + " --skip-reference Skip reference-data import (classes, sleeves, ranks, ...)\n" + " --skip-cards Skip card + card-cosmetic-reward import\n" + - " --skip-globals Skip prod-captured globals import\n" + + " --skip-globals Skip seed-driven globals import (per-table JSON under Data/seeds)\n" + " --story-data-dir Override story data directory (default: shipped Data/story)\n" + " --skip-story Skip story import (worlds/sections/chapters/sbs)\n" + "\n" + + "Capture-derived seeds are produced by extractors under data_dumps/extract/* and\n" + + "checked into SVSim.Bootstrap/Data/seeds/. The bootstrap project never parses wire\n" + + "captures directly — refresh seeds by re-running the relevant extractor.\n" + + "\n" + "Back-compat: `svsim-bootstrap [connection]` still works (positional)."); } private sealed record BootstrapOptions( string CardsFile, - string CapturesDir, string ReferenceDataDir, string ConnectionString, bool SkipReference, diff --git a/SVSim.Bootstrap/SVSim.Bootstrap.csproj b/SVSim.Bootstrap/SVSim.Bootstrap.csproj index 89f0861..e6f8608 100644 --- a/SVSim.Bootstrap/SVSim.Bootstrap.csproj +++ b/SVSim.Bootstrap/SVSim.Bootstrap.csproj @@ -10,9 +10,6 @@ - - PreserveNewest - PreserveNewest diff --git a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs index a1bca88..e9da421 100644 --- a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs +++ b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs @@ -176,21 +176,19 @@ internal sealed class SVSimTestFactory : WebApplicationFactory } /// - /// Runs against the test SQLite DB using the prod captures - /// copied into the test output dir (see SVSim.UnitTests.csproj Content Include for - /// Data/prod-captures). Idempotent — safe to call multiple times per factory. Tests that - /// depend on prod-shaped global content (spot_cards, avatar abilities, etc.) call this once - /// during setup; the rest of the test runs against whatever the importer populated. + /// Runs the per-domain seed importers against the test SQLite DB using the seed JSON + /// copied into the test output dir (see SVSim.UnitTests.csproj Content Includes for + /// Data/seeds and Data/test-fixtures/seeds). Idempotent — safe to call multiple times. + /// Tests that depend on prod-shaped global content (spot_cards, avatar abilities, etc.) + /// call this once during setup; the rest of the test runs against whatever the importers + /// populated. Mirrors the wiring in . /// - public async Task SeedGlobalsAsync(string? capturesDir = null) + public async Task SeedGlobalsAsync() { - capturesDir ??= Path.Combine(AppContext.BaseDirectory, "Data", "prod-captures"); string seedDir = Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); using var scope = Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); - await new GlobalsImporter().ImportAllAsync(ctx, capturesDir); - // Load-index seed pipeline (Stage 9C). Mirrors the wiring in SVSim.Bootstrap.Program.cs: // RotationConfigImporter must precede RotationFlagUpdater; CardListsImporter is // ordered after the GameConfig importers for tidiness (no FK dependency). await new RotationConfigImporter().ImportAsync(ctx, seedDir); @@ -203,7 +201,6 @@ internal sealed class SVSimTestFactory : WebApplicationFactory await new CardListsImporter().ImportAsync(ctx, seedDir); await new RotationFlagUpdater().UpdateAsync(ctx); - // Per-importer seed pipeline for the rest of the load-index split. await new PracticeOpponentImporter().ImportAsync(ctx, seedDir); await new PaymentItemImporter().ImportAsync(ctx, seedDir); var puzzleImporter = new PuzzleImporter(); diff --git a/SVSim.UnitTests/SVSim.UnitTests.csproj b/SVSim.UnitTests/SVSim.UnitTests.csproj index ec8616b..179e1b8 100644 --- a/SVSim.UnitTests/SVSim.UnitTests.csproj +++ b/SVSim.UnitTests/SVSim.UnitTests.csproj @@ -36,14 +36,8 @@ PreserveNewest - - - PreserveNewest - - + PreserveNewest @@ -54,14 +48,6 @@ PreserveNewest - - - PreserveNewest - PreserveNewest