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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user