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