feat(content): production content-pack loader + referenced-definition preflight

Production real-profile content is loaded via an explicit path, NOT the dev-only
OPENFUT_DEV_CONTENT_GAMES gate:
- Config.content_packs from env OPENFUT_CONTENT_PACKS (comma-sep file paths).
- CardDb::load_pack(path): merge an explicit CardDefinition[] production pack.
- app::build loads dev packs then production packs.

Preflight (app::build, always on): every owned_cards.card_id MUST resolve to a
loaded CardDefinition. A real profile with owned players but even ONE missing
definition fails LOUDLY instead of silently serving an empty /collection; an
empty owned_cards table (fresh DB / tests) passes.

2 preflight integration tests (missing def fails, loaded def passes). clippy
-D warnings clean; full suite 151 tests green.
This commit is contained in:
funman300
2026-08-12 19:49:14 +00:00
parent 3084a46dcc
commit 352ad11bc4
5 changed files with 201 additions and 9 deletions
+17
View File
@@ -62,6 +62,23 @@ impl CardDb {
Ok(n)
}
/// Merge an explicit PRODUCTION content pack file (a single
/// `CardDefinition[]`). Unlike [`CardDb::load_game_dev`] this takes a direct
/// path (the real-profile import emits one) and is the production content
/// path — not gated behind dev content. Returns the number merged.
pub fn load_pack(&mut self, path: &Path) -> Result<usize> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading content pack {path:?}"))?;
let batch: Vec<CardDefinition> =
serde_json::from_str(&content).with_context(|| format!("parsing {path:?}"))?;
let n = batch.len();
for card in batch {
self.cards.insert(card.id.clone(), card);
}
tracing::info!("Loaded {} production card definitions from {:?}", n, path);
Ok(n)
}
pub fn get(&self, id: &str) -> Option<&CardDefinition> {
self.cards.get(id)
}