feat(import-fifa17): --apply — recoverable two-store real-profile import
FIFA17-specific orchestration that installs the real profile across BOTH durable stores (openfut-identity + Core SQLite) recoverably and idempotently, handing Core only a GENERIC request (all FIFA17 semantics stay in this adapter layer). apply module: - owned_item_id(persona, wire) = deterministic UUIDv5 from a private namespace; identical in BOTH stores (identity core_id AND Core owned_cards.id), so the running host's wire->owned reverse lookup resolves exactly what Core stored. - plan_apply(report, raw_profile, fp): pure translation to a GenericImportRequest (card_id = fifa17_<resourceId>) + the preserved (owned_item_id <-> source wire) mappings + watermark. Canonical squad + Fifa17SquadExtensionV1 are built by the SAME adapter code (parse_squad_put + build_squad_write) the retail-validated live squad path uses. Refuses if the report has blockers. - Two-store protocol (apply): staging gate -> local Core-preflight mirror -> identity dry-preflight -> idempotent seed (insert_existing_mapping per instance + set_watermark) -> ONE generic Core import transaction (spawned binary) -> cross-store post-validation -> completion record. A crash after identity seeding re-converges on re-run (idempotent mappings + Core already_imported): no cleanup, no reminting. - gate_staging: deferred players are ABSENT from an import; allowed only for a staged run behind --allow-deferred-players-for-staging (never a silent default; prints an INCOMPLETE banner). Production requires zero deferred instances. CLI: --apply (with --emit-content, --core-bin, --core-db, --core-data, --identity-store, --allow-deferred-players-for-staging). Depends on openfut-adapter-fifa17 + openfut-identity + uuid(v5). Proven end-to-end on the real 33068179/CAGE profile (staged): first apply imports 1949 supported instances (293 base + versioned), 11/11 f433 squad + opaque extension, coins 28,112,944, fingerprint 8dc5582d2414af28; re-run is an idempotent no-op (already_imported, DB unchanged); staging-not-default refuses before any write; every OwnedItemId is an opaque UUID; identity wire-id set == source supported set exactly (0 minted, 0 dropped); 13 deferred instances leak 0. 10 new apply tests (determinism, request/mapping/squad translation, blocker refusal, staging gate both ways, local preflight, identity seed/dry/postvalidate/ idempotency, conflict detection, graceful spawn failure). clippy -D warnings clean; crate suite 23 tests green.
This commit is contained in:
@@ -10,6 +10,7 @@ use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use openfut_import_fifa17::apply::{apply, plan_apply, ApplyPaths};
|
||||
use openfut_import_fifa17::{
|
||||
analyze, emit_content, fingerprint, load_roster, model::Profile, Entities,
|
||||
};
|
||||
@@ -36,7 +37,12 @@ fn print_help() {
|
||||
--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"
|
||||
--apply apply across the identity store + Core (requires --emit-content)\n\
|
||||
--allow-deferred-players-for-staging import despite deferred players (STAGED only; never default)\n\
|
||||
--core-bin <path> openfut-core binary for the generic import transaction\n\
|
||||
--core-db <url> DATABASE_URL for the target Core DB\n\
|
||||
--core-data <dir> DATA_DIR base catalog for Core (default data)\n\
|
||||
--identity-store <path> openfut-identity JSON store to seed\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +52,12 @@ fn run() -> Result<ExitCode> {
|
||||
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 do_apply = false;
|
||||
let mut allow_staging = false;
|
||||
let mut core_bin: Option<String> = None;
|
||||
let mut core_db: Option<String> = None;
|
||||
let mut core_data = "data".to_string();
|
||||
let mut identity_store: Option<String> = None;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
@@ -62,9 +74,14 @@ fn run() -> Result<ExitCode> {
|
||||
);
|
||||
}
|
||||
"--dry-run" => {}
|
||||
"--apply" => bail!(
|
||||
"--apply is not implemented in this phase; refusing to write the Core DB or identity store"
|
||||
),
|
||||
"--apply" => do_apply = true,
|
||||
"--allow-deferred-players-for-staging" => allow_staging = true,
|
||||
"--core-bin" => core_bin = Some(args.next().context("--core-bin needs a path")?),
|
||||
"--core-db" => core_db = Some(args.next().context("--core-db needs a DATABASE_URL")?),
|
||||
"--core-data" => core_data = args.next().context("--core-data needs a dir")?,
|
||||
"--identity-store" => {
|
||||
identity_store = Some(args.next().context("--identity-store needs a path")?)
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
print_help();
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
@@ -108,6 +125,48 @@ fn run() -> Result<ExitCode> {
|
||||
);
|
||||
}
|
||||
|
||||
if do_apply {
|
||||
let dir = emit_dir
|
||||
.as_deref()
|
||||
.context("--apply requires --emit-content <dir> (it reads the emitted content pack)")?;
|
||||
if report.has_blockers() {
|
||||
bail!("not applying: unsafe blockers present (resolve them first)");
|
||||
}
|
||||
let raw_value: serde_json::Value =
|
||||
serde_json::from_slice(&raw).context("parse profile JSON for squad wire")?;
|
||||
let fp = fingerprint(&raw);
|
||||
let plan = plan_apply(&report, &raw_value, &fp)?;
|
||||
let base = Path::new(dir);
|
||||
let paths = ApplyPaths {
|
||||
core_bin: core_bin
|
||||
.context("--apply requires --core-bin <openfut-core binary>")?
|
||||
.into(),
|
||||
core_db_url: core_db.context("--apply requires --core-db <DATABASE_URL>")?,
|
||||
content_pack: base.join("content/fifa17-production-cards.json"),
|
||||
data_dir: core_data,
|
||||
identity_store: identity_store
|
||||
.context("--apply requires --identity-store <path>")?
|
||||
.into(),
|
||||
request_out: base.join("manifest/fifa17-core-import-request.json"),
|
||||
completion_out: base.join("manifest/fifa17-import-completion.json"),
|
||||
};
|
||||
let outcome = apply(&plan, &paths, allow_staging)?;
|
||||
println!(
|
||||
"\nAPPLIED (core_outcome={} staging={})",
|
||||
outcome.core_outcome, outcome.staging
|
||||
);
|
||||
println!(
|
||||
" identity mappings installed: {} ; watermark: {}",
|
||||
outcome.mappings_installed, outcome.watermark
|
||||
);
|
||||
println!(
|
||||
" supported instances: {} ; deferred instances: {}",
|
||||
outcome.supported_instances, outcome.deferred_instances
|
||||
);
|
||||
println!(" request : {}", paths.request_out.display());
|
||||
println!(" completion : {}", paths.completion_out.display());
|
||||
}
|
||||
|
||||
if report.has_blockers() {
|
||||
eprintln!("import-fifa17: FAILED — unsafe blockers present (see above)");
|
||||
Ok(ExitCode::FAILURE)
|
||||
|
||||
Reference in New Issue
Block a user