feat(import): --emit-content (production pack + host catalog + private manifest)

Fold entity resolution (nation/league/club id->name via committed tables,
mirroring seed_fifa17_cards.py) and quality-tier rarity into the analysis, so
the supported set is honest about unresolved entities too. Add an explicit
--defer-conflict <rid> allowlist: a reviewed conflict (169193) defers, any NEW
conflict still hard-fails (defer never becomes a silent conflict suppressor).

--emit-content writes three files, PUBLIC content separated from PRIVATE account
state: fifa17-production-cards.json (Core CardDefinition[] keyed fifa17_<resourceId>,
base+versioned, tier rarity, profile-derived, no promo labels), a versioned host
identity catalog {card_id:{asset_id,version}}, and a private import manifest
(supported instances' wire ids + deferred set with reasons + preserved watermark
+ target profile + snapshot fingerprint). Emit refuses while blockers exist.

Real profile (33068179/CAGE): 1681 supported defs (150 base + 1531 versioned),
9 NoName deferred, 1 approved-deferred conflict (169193, 4 copies), 1949
importable instances, watermark 100004617 -> next 100004617, active squad f433
11/11 supported. fmt + clippy -D warnings clean; 13 tests.
This commit is contained in:
funman300
2026-08-12 19:41:17 +00:00
parent a51947562c
commit c71c2a8d33
5 changed files with 720 additions and 316 deletions
+66 -19
View File
@@ -1,12 +1,19 @@
//! `openfut-import-fifa17` — real FIFA 17 profile import.
//!
//! This phase implements the **read-only dry-run** only. `--apply` and
//! `--emit-content` are recognised but deliberately refuse to run (and exit
//! nonzero) so nothing is written before the apply/emission phase lands.
//! This phase implements the read-only analysis and `--emit-content` (public
//! content pack + host catalog + private manifest). `--apply` is recognised but
//! deliberately refuses (and exits nonzero) so no Core DB / identity-store
//! writes happen before the apply phase lands.
use anyhow::{bail, Context, Result};
use std::collections::BTreeSet;
use std::path::Path;
use std::process::ExitCode;
use openfut_import_fifa17::{
analyze, emit_content, fingerprint, load_roster, model::Profile, Entities,
};
fn main() -> ExitCode {
match run() {
Ok(code) => code,
@@ -19,33 +26,44 @@ fn main() -> ExitCode {
fn print_help() {
eprintln!(
"openfut-import-fifa17 --profile <fifa17_profile.json> [--roster <roster.json>]\n\
"openfut-import-fifa17 --profile <fifa17_profile.json> [options]\n\
\n\
Read-only dry-run: analyses a real FIFA 17 Python profile for a faithful\n\
OpenFUT Core import and reports item accounting, observed definitions,\n\
identity preservation, and squad coverage. Exits nonzero on unsafe blockers.\n\
Read-only analysis of a real FIFA 17 Python profile for a faithful Core\n\
import, with optional public content emission. Exits nonzero on unsafe blockers.\n\
\n\
--profile <path> (required) source fifa17_profile.json\n\
--roster <path> roster.json for player names (default fifa17-recon/data/roster.json)\n\
--apply REFUSED in this phase (no writes)\n\
--emit-content <dir> REFUSED in this phase (no generated content)\n"
--profile <path> (required) source fifa17_profile.json\n\
--roster <path> roster.json for names (default fifa17-recon/data/roster.json)\n\
--tables <dir> tables dir (default fifa17-recon/data/tables)\n\
--defer-conflict <rid> explicitly approve deferring a reviewed resourceId conflict (repeatable)\n\
--emit-content <dir> write the production content pack + host catalog + private manifest\n\
--apply REFUSED in this phase (no Core/identity writes)\n"
);
}
fn run() -> Result<ExitCode> {
let mut profile_path: Option<String> = None;
let mut roster_path = "fifa17-recon/data/roster.json".to_string();
let mut tables_dir = "fifa17-recon/data/tables".to_string();
let mut emit_dir: Option<String> = None;
let mut approved: BTreeSet<i64> = BTreeSet::new();
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--profile" => profile_path = Some(args.next().context("--profile needs a path")?),
"--roster" => roster_path = args.next().context("--roster needs a path")?,
"--tables" => tables_dir = args.next().context("--tables needs a dir")?,
"--emit-content" => emit_dir = Some(args.next().context("--emit-content needs a dir")?),
"--defer-conflict" => {
let rid = args.next().context("--defer-conflict needs a resourceId")?;
approved.insert(
rid.parse::<i64>()
.with_context(|| format!("--defer-conflict {rid} is not an integer"))?,
);
}
"--dry-run" => {}
"--apply" => bail!(
"--apply is not implemented in this phase (dry-run + identity import API only); refusing to write the Core DB or identity store"
),
"--emit-content" => bail!(
"--emit-content is not implemented in this phase; refusing to write generated content"
"--apply is not implemented in this phase; refusing to write the Core DB or identity store"
),
"-h" | "--help" => {
print_help();
@@ -56,13 +74,42 @@ fn run() -> Result<ExitCode> {
}
let profile_path = profile_path.context("--profile <path> is required (try --help)")?;
let profile = openfut_import_fifa17::load_profile(&profile_path)?;
let roster = openfut_import_fifa17::load_roster(&roster_path)?;
let report = openfut_import_fifa17::analyze(&profile, &roster);
let raw =
std::fs::read(&profile_path).with_context(|| format!("reading profile {profile_path}"))?;
let profile = Profile::from_json_str(&String::from_utf8_lossy(&raw))?;
let roster = load_roster(&roster_path)?;
let entities = Entities::from_tables_dir(&tables_dir)?;
let report = analyze(&profile, &roster, &entities, &approved);
print!("{report}");
if let Some(dir) = &emit_dir {
if report.has_blockers() {
bail!("not emitting content: unsafe blockers present (resolve them first)");
}
let fp = fingerprint(&raw);
let sum = emit_content(&report, Path::new(dir), &fp)?;
println!("\nEMITTED (fingerprint {fp})");
println!(
" content pack : {} ({} definitions)",
sum.content_pack.display(),
sum.definitions
);
println!(
" host catalog : {} ({} entries)",
sum.host_catalog.display(),
sum.catalog_entries
);
println!(
" manifest : {} (supported_instances={} deferred_instances={})",
sum.manifest.display(),
sum.supported_instances,
sum.deferred_instances
);
}
if report.has_blockers() {
eprintln!("import-fifa17: DRY-RUN FAILED — unsafe blockers present (see above)");
eprintln!("import-fifa17: FAILED — unsafe blockers present (see above)");
Ok(ExitCode::FAILURE)
} else {
Ok(ExitCode::SUCCESS)