Files
OpenFUT/openfut-utas-host
funman300 3442eac6f0 fix(fifa17): complete kit stats, restore red squad tests, unrot prod gate
Four defects found by running the suites and the staging lifecycle end to end
after the kit milestone.

1. club-stats kits were half-implemented. The global `kits` counter was real
   but `kitsHome`/`kitsAway` and every per-team `kits` bucket stayed hardcoded
   0, so the same screen reported two owned kits and zero home/away kits.
   `kits` is a total with a family split, exactly like players/playersGold and
   staff/staffManager. The split key is `fcc_kitcards.assetid`: 14 is the home
   family and 15 the away family, verified across all 1482 rows of the kit
   table (assetid 14 covers exactly the 63xxxxx carddbids, 828 rows; assetid 15
   exactly the 64xxxxx ones, 654 rows; no exceptions either way).
   ClubStatInput now carries `asset_id`, and a kit buckets onto the team that
   wears it -- including a team the club owns no player from, the normal case
   for a kit won from a pack. The host reads both from the catalog through new
   NON-MINTING accessors: `resolve`/`resolve_kit` allocate a wire id, which a
   read-only stats query must never do as a side effect.

2. host_test.rs had 10 tests red since the squad-manager work (25f4ad1 /
   d37a9d5); 56bd9dd updated the squad_projection integration test and stopped
   there. `put_body` hardcoded the captured manager ref 100000427 into EVERY
   save, including tests with no manager fixture, so each one was refused with
   `unresolved_wire_ids` -- the tests were reporting a real invariant against a
   fixture that could not satisfy it. The manager is now an explicit
   `Option<i64>` per test, and FakeCore models Core's manager persistence
   instead of inheriting the "not implemented" default that 502'd every save.
   Added the coverage whose absence let this rot: a manager assignment
   round-trips as a Core owned id, a later save without one CLEARS it, and an
   unowned manager ref refuses the whole save with nothing committed.

3. `club_route_maps_query_and_shapes_core_items` pinned `offset`/`limit`
   forwarding to Core, which the kit commit deliberately replaced with
   host-side pagination. It only ever passed because FakeCore ignored the
   window -- against a real Core, `start=10` over a one-item club was always an
   empty page. Retargeted to the real contract (Core gets semantic filters and
   NO window) plus a new test that the window is applied locally after
   filtering, which the old fake made vacuous.

4. The staging lifecycle scripts identified production by hardcoded pids, so a
   correct teardown FATAL'd: production moved into containers and pids
   3631953/3374264 died with a container restart days ago. A pinned pid rots
   into the worst of both worlds -- a kill-refusal gate that no longer names
   any real production process, and a liveness gate that fails a healthy
   teardown. New shared `scripts/openfut_production.py` resolves production
   pids AND published ports from the container runtime at the moment they are
   needed, refuses to signal anything it cannot see, and proves production is
   the same processes serving the same ports before and after. Both lifecycle
   scripts use it, which also closed a real gap: port 8085 is published by
   openfut-fut-backend but was missing from the up script's forbidden list, so
   staging could have bound a production port.

Also fixes the economy differential, red because `complete_match` unlocks
achievements in the same transaction that pays the match reward -- a deliberate
Core feature the Python oracle has no counterpart for. `rust WIN +400` asserted
that progression did not exist; it now asserts the delta is the 400 match reward
plus exactly the achievements the match unlocked, read from Core's own report.
2026-08-21 04:10:34 +00:00
..

openfut-utas-host

The FIFA 17 UTAS migration boundary. It accepts the client-visible HTTP surface, serves migrated routes from Rust/Core plus host-owned durable stores, and proxies only the unclassified tail to the Python behavioral oracle.

FIFA 17 ──HTTP──▶ openfut-utas-host
                    ├── migrated route ──▶ Rust adapter / Core / host stores
                    └── unclassified tail ──▶ Python UTAS oracle

src/lib.rs::classify is the route-level source of truth. The current Rust surface includes club/squad/user reads, club rename, auth/session/client data, Store/economy, packs, owned-item moves, market/trade-pile, and the observed hub support routes.

Safety model

  • Classification happens exactly once before execution. There is no "try Rust then Python"; a mutation cannot be double-applied.
  • A route classified to Rust never falls back to Python on a Core/store/projection failure. Each handler uses its captured fail-closed or honest-empty wire contract.
  • PUT …/club and PUT|POST …/user/club atomically update the shared account JSON. Every input returns the required zero-atom 200 {} response; rejection and persistence failures remain visible in logs.
  • Numeric GET …/squad/<n> returns the one Core-backed current squad, matching the Python oracle's single-current-squad behavior.
  • Python remains the behavioral oracle and rollback backend for routes not yet classified to Rust. New economy behavior belongs in Rust/Core, never Python.

Configuration (env)

Var Required Default Meaning
OPENFUT_UTAS_HOST_ADDR yes client-visible host listen address
OPENFUT_UTAS_PYTHON_URL yes Python oracle base for the unclassified tail; must differ from this host
OPENFUT_FIFA17_CATALOG yes FIFA 17 definition identity catalog
OPENFUT_IDENTITY_STORE yes persistent owned-instance ↔ wire-id store
OPENFUT_PERSONA_ID yes non-zero FIFA persona id shared by LSX/Blaze/POW/UTAS
OPENFUT_MARKET_DB yes durable host-owned transfer-market SQLite DB
OPENFUT_PILE_DB yes durable host-owned item-pile SQLite DB
OPENFUT_CORE_URL no http://127.0.0.1:8080 OpenFUT Core base
OPENFUT_FIFA17_TABLES_DIR no fifa17-recon/data/tables FIFA entity tables
OPENFUT_CLIENTDATA_DB no identity-store sibling clientdata.json durable opaque client-data JSON
OPENFUT_ACCOUNT_PATH no FUT_ACCOUNT_PATH, then identity-store sibling active_account.json shared FIFA account/club JSON

Startup fails if required identity or durable economy state cannot be opened. No placeholder production identity source is substituted.

Identity model (resolved)

FIFA renders an owned card by resolving resourceId & 0xffffff against the client's own local players table; an invented id renders a blank generic card (proven live — fut_cards.py:11-21). Two distinct identities, never conflated, are resolved by [Fifa17IdentityResolver] (the single production path):

  • Definition identity (resourceId/assetId) — the card's real FIFA asset id, from the versioned OPENFUT_FIFA17_CATALOG. An unmapped definition is dropped and counted, never faked.
  • Instance identity (id) — a stable, persistent, reversible wire integer from the generic openfut-identity store under the FIFA 17 wire-id policy (monotonic from 100_000_001). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share a resourceId but get distinct ids. The namespace is globally monotonic within (fifa17, owned-item) — no per-account column is needed because Core owned-instance ids are globally-unique UUIDs.

Production has a frozen post-P1 baseline and a hot Python rollback. A source change passing local tests is not deployment approval. Build verification, staging, host restart, and live-client promotion remain operator-gated; the current state and promotion evidence live in the OpenFUT Obsidian vault.

Logs are safe by construction: no auth/session/device/token material — only owner, route, filter summary, counts, status.